summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-02-18 10:34:06 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-02-18 10:34:06 +0000
commit859a6fb938bb9ee2a317c46dfa4fcc1af49608f0 (patch)
treed7f2700abe6b4ffcb2dcfc80631b2d87d0609239 /spec
parent446d496a6d000c73a304be52587cd9bbc7493136 (diff)
downloadgitlab-ce-859a6fb938bb9ee2a317c46dfa4fcc1af49608f0.tar.gz
Add latest changes from gitlab-org/gitlab@13-9-stable-eev13.9.0-rc42
Diffstat (limited to 'spec')
-rw-r--r--spec/benchmarks/banzai_benchmark.rb124
-rw-r--r--spec/config/object_store_settings_spec.rb69
-rw-r--r--spec/controllers/admin/application_settings_controller_spec.rb7
-rw-r--r--spec/controllers/admin/cohorts_controller_spec.rb34
-rw-r--r--spec/controllers/admin/runners_controller_spec.rb3
-rw-r--r--spec/controllers/admin/users_controller_spec.rb7
-rw-r--r--spec/controllers/chaos_controller_spec.rb19
-rw-r--r--spec/controllers/concerns/redis_tracking_spec.rb115
-rw-r--r--spec/controllers/concerns/spammable_actions_spec.rb99
-rw-r--r--spec/controllers/graphql_controller_spec.rb20
-rw-r--r--spec/controllers/groups/group_members_controller_spec.rb12
-rw-r--r--spec/controllers/groups_controller_spec.rb60
-rw-r--r--spec/controllers/help_controller_spec.rb110
-rw-r--r--spec/controllers/import/bulk_imports_controller_spec.rb24
-rw-r--r--spec/controllers/invites_controller_spec.rb43
-rw-r--r--spec/controllers/profiles/notifications_controller_spec.rb3
-rw-r--r--spec/controllers/profiles/preferences_controller_spec.rb35
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb4
-rw-r--r--spec/controllers/projects/ci/daily_build_group_report_results_controller_spec.rb43
-rw-r--r--spec/controllers/projects/discussions_controller_spec.rb7
-rw-r--r--spec/controllers/projects/forks_controller_spec.rb22
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb57
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb48
-rw-r--r--spec/controllers/projects/learn_gitlab_controller_spec.rb44
-rw-r--r--spec/controllers/projects/merge_requests/diffs_controller_spec.rb8
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb105
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb87
-rw-r--r--spec/controllers/projects/pipelines/tests_controller_spec.rb24
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb66
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb12
-rw-r--r--spec/controllers/projects/security/configuration_controller_spec.rb55
-rw-r--r--spec/controllers/projects/templates_controller_spec.rb3
-rw-r--r--spec/controllers/projects_controller_spec.rb61
-rw-r--r--spec/controllers/registrations/experience_levels_controller_spec.rb127
-rw-r--r--spec/controllers/registrations_controller_spec.rb12
-rw-r--r--spec/controllers/repositories/git_http_controller_spec.rb33
-rw-r--r--spec/controllers/search_controller_spec.rb392
-rw-r--r--spec/controllers/snippets_controller_spec.rb2
-rw-r--r--spec/db/schema_spec.rb2
-rw-r--r--spec/deprecation_toolkit_env.rb94
-rw-r--r--spec/experiments/application_experiment/cache_spec.rb12
-rw-r--r--spec/experiments/application_experiment_spec.rb147
-rw-r--r--spec/experiments/members/invite_email_experiment_spec.rb37
-rw-r--r--spec/experiments/new_project_readme_experiment_spec.rb93
-rw-r--r--spec/experiments/strategy/round_robin_spec.rb68
-rw-r--r--spec/factories/audit_events.rb15
-rw-r--r--spec/factories/ci/bridge.rb18
-rw-r--r--spec/factories/ci/builds.rb32
-rw-r--r--spec/factories/ci/daily_build_group_report_results.rb1
-rw-r--r--spec/factories/ci/job_artifacts.rb20
-rw-r--r--spec/factories/ci/pipeline_artifacts.rb33
-rw-r--r--spec/factories/ci/pipelines.rb24
-rw-r--r--spec/factories/ci/processable.rb26
-rw-r--r--spec/factories/ci/reports/codequality_degradations.rb98
-rw-r--r--spec/factories/ci/resource.rb2
-rw-r--r--spec/factories/design_management/design_at_version.rb2
-rw-r--r--spec/factories/diff_position.rb2
-rw-r--r--spec/factories/merge_request_diffs.rb6
-rw-r--r--spec/factories/merge_requests.rb45
-rw-r--r--spec/factories/notes.rb16
-rw-r--r--spec/factories/packages.rb54
-rw-r--r--spec/factories/packages/debian/component_file.rb39
-rw-r--r--spec/factories/packages/debian/distribution.rb2
-rw-r--r--spec/factories/packages/debian/group_component.rb9
-rw-r--r--spec/factories/packages/debian/project_component.rb9
-rw-r--r--spec/factories/packages/debian/publication.rb9
-rw-r--r--spec/factories/packages/package_file.rb16
-rw-r--r--spec/factories/packages/rubygems/metadata.rb9
-rw-r--r--spec/factories/pages_deployments.rb18
-rw-r--r--spec/factories/projects.rb4
-rw-r--r--spec/factories/sequences.rb2
-rw-r--r--spec/factories/services_data.rb3
-rw-r--r--spec/factories/token_with_ivs.rb9
-rw-r--r--spec/factories/u2f_registrations.rb2
-rw-r--r--spec/factories/usage_data.rb4
-rw-r--r--spec/features/admin/admin_cohorts_spec.rb33
-rw-r--r--spec/features/admin/admin_disables_git_access_protocol_spec.rb15
-rw-r--r--spec/features/admin/admin_groups_spec.rb2
-rw-r--r--spec/features/admin/admin_projects_spec.rb99
-rw-r--r--spec/features/admin/admin_search_settings_spec.rb27
-rw-r--r--spec/features/admin/admin_settings_spec.rb75
-rw-r--r--spec/features/admin/admin_users_spec.rb70
-rw-r--r--spec/features/admin/users/user_spec.rb4
-rw-r--r--spec/features/alert_management/alert_details_spec.rb2
-rw-r--r--spec/features/alerts_settings/user_views_alerts_settings_spec.rb3
-rw-r--r--spec/features/boards/boards_spec.rb8
-rw-r--r--spec/features/boards/sidebar_spec.rb44
-rw-r--r--spec/features/calendar_spec.rb2
-rw-r--r--spec/features/commit_spec.rb78
-rw-r--r--spec/features/commits_spec.rb3
-rw-r--r--spec/features/dashboard/activity_spec.rb20
-rw-r--r--spec/features/discussion_comments/issue_spec.rb2
-rw-r--r--spec/features/discussion_comments/merge_request_spec.rb1
-rw-r--r--spec/features/discussion_comments/snippets_spec.rb29
-rw-r--r--spec/features/groups/import_export/connect_instance_spec.rb16
-rw-r--r--spec/features/groups/navbar_spec.rb8
-rw-r--r--spec/features/groups/settings/packages_and_registries_spec.rb68
-rw-r--r--spec/features/groups/show_spec.rb1
-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/import/manifest_import_spec.rb2
-rw-r--r--spec/features/issuables/issuable_list_spec.rb2
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb10
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb35
-rw-r--r--spec/features/issues/issue_state_spec.rb26
-rw-r--r--spec/features/issues/user_comments_on_issue_spec.rb4
-rw-r--r--spec/features/issues/user_creates_issue_by_email_spec.rb4
-rw-r--r--spec/features/issues/user_resets_their_incoming_email_token_spec.rb24
-rw-r--r--spec/features/issues/user_toggles_subscription_spec.rb6
-rw-r--r--spec/features/labels_hierarchy_spec.rb5
-rw-r--r--spec/features/markdown/markdown_spec.rb27
-rw-r--r--spec/features/markdown/mermaid_spec.rb2
-rw-r--r--spec/features/merge_request/user_accepts_merge_request_spec.rb2
-rw-r--r--spec/features/merge_request/user_closes_reopens_merge_request_state_spec.rb37
-rw-r--r--spec/features/merge_request/user_edits_mr_spec.rb10
-rw-r--r--spec/features/merge_request/user_manages_subscription_spec.rb2
-rw-r--r--spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb4
-rw-r--r--spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb8
-rw-r--r--spec/features/merge_request/user_posts_notes_spec.rb2
-rw-r--r--spec/features/merge_request/user_reverts_merge_request_spec.rb43
-rw-r--r--spec/features/merge_request/user_sees_cherry_pick_modal_spec.rb14
-rw-r--r--spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb4
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb25
-rw-r--r--spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb234
-rw-r--r--spec/features/merge_request/user_suggests_changes_on_diff_spec.rb17
-rw-r--r--spec/features/merge_request/user_views_open_merge_request_spec.rb29
-rw-r--r--spec/features/merge_requests/user_filters_by_milestones_spec.rb2
-rw-r--r--spec/features/merge_requests/user_lists_merge_requests_spec.rb12
-rw-r--r--spec/features/profiles/user_edit_preferences_spec.rb11
-rw-r--r--spec/features/profiles/user_edit_profile_spec.rb32
-rw-r--r--spec/features/profiles/user_search_settings_spec.rb25
-rw-r--r--spec/features/profiles/user_visits_notifications_tab_spec.rb1
-rw-r--r--spec/features/profiles/user_visits_profile_preferences_page_spec.rb12
-rw-r--r--spec/features/profiles/user_visits_profile_spec.rb2
-rw-r--r--spec/features/projects/branches_spec.rb10
-rw-r--r--spec/features/projects/commit/cherry_pick_spec.rb160
-rw-r--r--spec/features/projects/commit/user_reverts_commit_spec.rb91
-rw-r--r--spec/features/projects/commits/user_browses_commits_spec.rb9
-rw-r--r--spec/features/projects/compare_spec.rb23
-rw-r--r--spec/features/projects/features_visibility_spec.rb6
-rw-r--r--spec/features/projects/files/dockerfile_dropdown_spec.rb8
-rw-r--r--spec/features/projects/files/gitignore_dropdown_spec.rb10
-rw-r--r--spec/features/projects/files/gitlab_ci_syntax_yml_dropdown_spec.rb7
-rw-r--r--spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb10
-rw-r--r--spec/features/projects/fork_spec.rb39
-rw-r--r--spec/features/projects/graph_spec.rb6
-rw-r--r--spec/features/projects/issuable_templates_spec.rb4
-rw-r--r--spec/features/projects/jobs_spec.rb52
-rw-r--r--spec/features/projects/members/anonymous_user_sees_members_spec.rb26
-rw-r--r--spec/features/projects/members/group_members_spec.rb243
-rw-r--r--spec/features/projects/members/groups_with_access_list_spec.rb180
-rw-r--r--spec/features/projects/members/invite_group_spec.rb86
-rw-r--r--spec/features/projects/members/list_spec.rb222
-rw-r--r--spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb111
-rw-r--r--spec/features/projects/members/sorting_spec.rb204
-rw-r--r--spec/features/projects/members/tabs_spec.rb99
-rw-r--r--spec/features/projects/navbar_spec.rb19
-rw-r--r--spec/features/projects/pages/user_adds_domain_spec.rb185
-rw-r--r--spec/features/projects/pages/user_edits_lets_encrypt_settings_spec.rb (renamed from spec/features/projects/pages_lets_encrypt_spec.rb)2
-rw-r--r--spec/features/projects/pages/user_edits_settings_spec.rb201
-rw-r--r--spec/features/projects/pages_spec.rb411
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb221
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb113
-rw-r--r--spec/features/projects/services/user_activates_slack_slash_command_spec.rb4
-rw-r--r--spec/features/projects/settings/pipelines_settings_spec.rb2
-rw-r--r--spec/features/projects/settings/project_settings_spec.rb4
-rw-r--r--spec/features/projects/settings/registry_settings_spec.rb4
-rw-r--r--spec/features/projects/settings/repository_settings_spec.rb6
-rw-r--r--spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb6
-rw-r--r--spec/features/projects/settings/user_manages_project_members_spec.rb134
-rw-r--r--spec/features/projects/settings/visibility_settings_spec.rb4
-rw-r--r--spec/features/projects/show/user_manages_notifications_spec.rb1
-rw-r--r--spec/features/projects/show/user_sees_git_instructions_spec.rb7
-rw-r--r--spec/features/projects/terraform_spec.rb2
-rw-r--r--spec/features/projects/user_creates_project_spec.rb6
-rw-r--r--spec/features/projects/user_sees_user_popover_spec.rb2
-rw-r--r--spec/features/projects/user_uses_shortcuts_spec.rb7
-rw-r--r--spec/features/protected_branches_spec.rb6
-rw-r--r--spec/features/registrations/experience_level_spec.rb1
-rw-r--r--spec/features/search/user_searches_for_commits_spec.rb4
-rw-r--r--spec/features/search/user_searches_for_issues_spec.rb2
-rw-r--r--spec/features/search/user_searches_for_merge_requests_spec.rb34
-rw-r--r--spec/features/search/user_searches_for_projects_spec.rb2
-rw-r--r--spec/features/task_lists_spec.rb8
-rw-r--r--spec/features/u2f_spec.rb10
-rw-r--r--spec/features/user_sees_revert_modal_spec.rb27
-rw-r--r--spec/features/users/login_spec.rb2
-rw-r--r--spec/features/users/overview_spec.rb126
-rw-r--r--spec/features/users/show_spec.rb52
-rw-r--r--spec/features/webauthn_spec.rb4
-rw-r--r--spec/features/whats_new_spec.rb35
-rw-r--r--spec/finders/autocomplete/users_finder_spec.rb5
-rw-r--r--spec/finders/ci/jobs_finder_spec.rb33
-rw-r--r--spec/finders/ci/testing/daily_build_group_report_results_finder_spec.rb99
-rw-r--r--spec/finders/container_repositories_finder_spec.rb30
-rw-r--r--spec/finders/deployments_finder_spec.rb207
-rw-r--r--spec/finders/license_template_finder_spec.rb43
-rw-r--r--spec/finders/merge_request/metrics_finder_spec.rb76
-rw-r--r--spec/finders/merge_requests/oldest_per_commit_finder_spec.rb46
-rw-r--r--spec/finders/packages/group_packages_finder_spec.rb1
-rw-r--r--spec/finders/packages/packages_finder_spec.rb1
-rw-r--r--spec/finders/repositories/commits_with_trailer_finder_spec.rb38
-rw-r--r--spec/finders/repositories/previous_tag_finder_spec.rb41
-rw-r--r--spec/finders/template_finder_spec.rb192
-rw-r--r--spec/finders/terraform/states_finder_spec.rb45
-rw-r--r--spec/finders/user_recent_events_finder_spec.rb120
-rw-r--r--spec/fixtures/api/schemas/entities/codequality_mr_diff_report.json21
-rw-r--r--spec/fixtures/api/schemas/entities/member.json4
-rw-r--r--spec/fixtures/api/schemas/entities/member_user.json1
-rw-r--r--spec/fixtures/api/schemas/graphql/packages/package_composer_details.json12
-rw-r--r--spec/fixtures/api/schemas/graphql/packages/package_composer_metadata.json21
-rw-r--r--spec/fixtures/api/schemas/graphql/packages/package_details.json38
-rw-r--r--spec/fixtures/api/schemas/group_group_links.json6
-rw-r--r--spec/fixtures/api/schemas/group_link/group_group_link.json16
-rw-r--r--spec/fixtures/api/schemas/group_link/group_group_links.json6
-rw-r--r--spec/fixtures/api/schemas/group_link/group_link.json (renamed from spec/fixtures/api/schemas/entities/group_group_link.json)10
-rw-r--r--spec/fixtures/api/schemas/group_link/project_group_link.json16
-rw-r--r--spec/fixtures/api/schemas/group_link/project_group_links.json6
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/job.json4
-rw-r--r--spec/fixtures/markdown.md.erb22
-rw-r--r--spec/fixtures/packages/composer/package.json1
-rw-r--r--spec/fixtures/packages/debian/distribution/Packages2
-rw-r--r--spec/fixtures/packages/debian/distribution/Release1
-rw-r--r--spec/fixtures/packages/rubygems/package-0.0.1.gembin0 -> 4096 bytes
-rw-r--r--spec/fixtures/packages/rubygems/package.gemspec15
-rw-r--r--spec/fixtures/pipeline_artifacts/code_quality_mr_diff.json23
-rw-r--r--spec/fixtures/security_reports/master/gl-sast-report.json983
-rw-r--r--spec/fixtures/security_reports/master/gl-secret-detection-report.json33
-rw-r--r--spec/fixtures/whats_new/invalid.yml2
-rw-r--r--spec/fixtures/whats_new/valid.yml2
-rw-r--r--spec/frontend/__helpers__/emoji.js36
-rw-r--r--spec/frontend/__helpers__/fake_date/fake_date.js (renamed from spec/frontend/__helpers__/fake_date.js)21
-rw-r--r--spec/frontend/__helpers__/fake_date/fake_date_spec.js (renamed from spec/frontend/__helpers__/fake_date_spec.js)8
-rw-r--r--spec/frontend/__helpers__/fake_date/index.js2
-rw-r--r--spec/frontend/__helpers__/fake_date/jest.js41
-rw-r--r--spec/frontend/__helpers__/graphql_helpers.js14
-rw-r--r--spec/frontend/__helpers__/graphql_helpers_spec.js23
-rw-r--r--spec/frontend/__helpers__/init_vue_mr_page_helper.js4
-rw-r--r--spec/frontend/__helpers__/jest_execution_watcher.js12
-rw-r--r--spec/frontend/__helpers__/stub_component.js25
-rw-r--r--spec/frontend/__mocks__/@gitlab/ui.js4
-rw-r--r--spec/frontend/access_tokens/components/expires_at_field_spec.js3
-rw-r--r--spec/frontend/actioncable_connection_monitor_spec.js79
-rw-r--r--spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js8
-rw-r--r--spec/frontend/add_context_commits_modal/components/review_tab_container_spec.js2
-rw-r--r--spec/frontend/add_context_commits_modal/store/actions_spec.js2
-rw-r--r--spec/frontend/add_context_commits_modal/store/mutations_spec.js2
-rw-r--r--spec/frontend/admin/statistics_panel/components/app_spec.js8
-rw-r--r--spec/frontend/admin/statistics_panel/store/actions_spec.js2
-rw-r--r--spec/frontend/admin/statistics_panel/store/getters_spec.js2
-rw-r--r--spec/frontend/admin/statistics_panel/store/mutations_spec.js2
-rw-r--r--spec/frontend/admin/users/components/actions/actions_spec.js98
-rw-r--r--spec/frontend/admin/users/components/user_actions_spec.js158
-rw-r--r--spec/frontend/admin/users/components/user_avatar_spec.js85
-rw-r--r--spec/frontend/admin/users/components/user_date_spec.js34
-rw-r--r--spec/frontend/admin/users/components/users_table_spec.js28
-rw-r--r--spec/frontend/admin/users/constants.js19
-rw-r--r--spec/frontend/admin/users/index_spec.js4
-rw-r--r--spec/frontend/admin/users/mock_data.js1
-rw-r--r--spec/frontend/alert_management/components/alert_management_empty_state_spec.js2
-rw-r--r--spec/frontend/alert_management/components/alert_management_list_wrapper_spec.js2
-rw-r--r--spec/frontend/alert_management/components/alert_management_table_spec.js8
-rw-r--r--spec/frontend/alerts_service_settings/components/__snapshots__/alerts_service_form_spec.js.snap9
-rw-r--r--spec/frontend/alerts_service_settings/components/alerts_service_form_spec.js152
-rw-r--r--spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_spec.js.snap98
-rw-r--r--spec/frontend/alerts_settings/components/__snapshots__/alerts_settings_form_spec.js.snap406
-rw-r--r--spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js (renamed from spec/frontend/alerts_settings/alert_mapping_builder_spec.js)49
-rw-r--r--spec/frontend/alerts_settings/components/alerts_integrations_list_spec.js (renamed from spec/frontend/alerts_settings/alerts_integrations_list_spec.js)2
-rw-r--r--spec/frontend/alerts_settings/components/alerts_settings_form_spec.js (renamed from spec/frontend/alerts_settings/alerts_settings_form_spec.js)242
-rw-r--r--spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js (renamed from spec/frontend/alerts_settings/alerts_settings_wrapper_spec.js)24
-rw-r--r--spec/frontend/alerts_settings/components/mocks/apollo_mock.js (renamed from spec/frontend/alerts_settings/mocks/apollo_mock.js)0
-rw-r--r--spec/frontend/alerts_settings/components/mocks/integrations.json (renamed from spec/frontend/alerts_settings/mocks/integrations.json)0
-rw-r--r--spec/frontend/alerts_settings/components/util.js (renamed from spec/frontend/alerts_settings/util.js)0
-rw-r--r--spec/frontend/alerts_settings/mocks/alertFields.json123
-rw-r--r--spec/frontend/alerts_settings/utils/mapping_transformations_spec.js81
-rw-r--r--spec/frontend/analytics/components/activity_chart_spec.js2
-rw-r--r--spec/frontend/analytics/instance_statistics/components/app_spec.js4
-rw-r--r--spec/frontend/analytics/instance_statistics/components/instance_statistics_count_chart_spec.js6
-rw-r--r--spec/frontend/analytics/instance_statistics/components/projects_and_groups_chart_spec.js15
-rw-r--r--spec/frontend/analytics/instance_statistics/components/users_chart_spec.js13
-rw-r--r--spec/frontend/analytics/shared/components/metric_card_spec.js2
-rw-r--r--spec/frontend/api/api_utils_spec.js4
-rw-r--r--spec/frontend/api_spec.js24
-rw-r--r--spec/frontend/artifacts_settings/components/__snapshots__/keep_latest_artifact_checkbox_spec.js.snap55
-rw-r--r--spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js96
-rw-r--r--spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js8
-rw-r--r--spec/frontend/authentication/two_factor_auth/index_spec.js4
-rw-r--r--spec/frontend/avatar_helper_spec.js2
-rw-r--r--spec/frontend/awards_handler_spec.js20
-rw-r--r--spec/frontend/badges/components/badge_form_spec.js10
-rw-r--r--spec/frontend/badges/components/badge_list_row_spec.js2
-rw-r--r--spec/frontend/badges/components/badge_list_spec.js2
-rw-r--r--spec/frontend/badges/components/badge_settings_spec.js8
-rw-r--r--spec/frontend/badges/store/actions_spec.js4
-rw-r--r--spec/frontend/batch_comments/components/draft_note_spec.js23
-rw-r--r--spec/frontend/batch_comments/components/preview_item_spec.js19
-rw-r--r--spec/frontend/batch_comments/components/publish_dropdown_spec.js4
-rw-r--r--spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js2
-rw-r--r--spec/frontend/batch_comments/stores/modules/batch_comments/mutations_spec.js4
-rw-r--r--spec/frontend/behaviors/autosize_spec.js32
-rw-r--r--spec/frontend/behaviors/copy_as_gfm_spec.js2
-rw-r--r--spec/frontend/behaviors/gl_emoji_spec.js4
-rw-r--r--spec/frontend/blob/balsamiq/balsamiq_viewer_spec.js2
-rw-r--r--spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap2
-rw-r--r--spec/frontend/blob/components/blob_content_error_spec.js2
-rw-r--r--spec/frontend/blob/components/blob_content_spec.js4
-rw-r--r--spec/frontend/blob/components/blob_edit_header_spec.js2
-rw-r--r--spec/frontend/blob/components/blob_header_default_actions_spec.js2
-rw-r--r--spec/frontend/blob/components/blob_header_filepath_spec.js2
-rw-r--r--spec/frontend/blob/components/blob_header_spec.js2
-rw-r--r--spec/frontend/blob/components/blob_header_viewer_switcher_spec.js2
-rw-r--r--spec/frontend/blob/notebook/notebook_viever_spec.js4
-rw-r--r--spec/frontend/blob/pdf/pdf_viewer_spec.js2
-rw-r--r--spec/frontend/blob/pipeline_tour_success_modal_spec.js4
-rw-r--r--spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js2
-rw-r--r--spec/frontend/blob/utils_spec.js2
-rw-r--r--spec/frontend/blob/viewer/index_spec.js2
-rw-r--r--spec/frontend/blob_edit/edit_blob_spec.js2
-rw-r--r--spec/frontend/boards/board_list_deprecated_spec.js11
-rw-r--r--spec/frontend/boards/board_list_helper.js8
-rw-r--r--spec/frontend/boards/board_list_spec.js8
-rw-r--r--spec/frontend/boards/board_new_issue_deprecated_spec.js4
-rw-r--r--spec/frontend/boards/boards_store_spec.js6
-rw-r--r--spec/frontend/boards/boards_util_spec.js17
-rw-r--r--spec/frontend/boards/components/board_assignee_dropdown_spec.js380
-rw-r--r--spec/frontend/boards/components/board_card_layout_deprecated_spec.js158
-rw-r--r--spec/frontend/boards/components/board_card_layout_spec.js67
-rw-r--r--spec/frontend/boards/components/board_card_spec.js10
-rw-r--r--spec/frontend/boards/components/board_column_deprecated_spec.js16
-rw-r--r--spec/frontend/boards/components/board_column_spec.js13
-rw-r--r--spec/frontend/boards/components/board_configuration_options_spec.js15
-rw-r--r--spec/frontend/boards/components/board_content_spec.js6
-rw-r--r--spec/frontend/boards/components/board_form_spec.js55
-rw-r--r--spec/frontend/boards/components/board_list_header_deprecated_spec.js12
-rw-r--r--spec/frontend/boards/components/board_list_header_spec.js12
-rw-r--r--spec/frontend/boards/components/board_new_issue_spec.js2
-rw-r--r--spec/frontend/boards/components/board_settings_sidebar_spec.js10
-rw-r--r--spec/frontend/boards/components/boards_selector_deprecated_spec.js214
-rw-r--r--spec/frontend/boards/components/boards_selector_spec.js30
-rw-r--r--spec/frontend/boards/components/issue_time_estimate_spec.js2
-rw-r--r--spec/frontend/boards/components/sidebar/board_editable_item_spec.js2
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_due_date_spec.js4
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_issue_title_spec.js6
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js6
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_milestone_select_spec.js50
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js8
-rw-r--r--spec/frontend/boards/components/sidebar/remove_issue_spec.js2
-rw-r--r--spec/frontend/boards/issue_card_deprecated_spec.js4
-rw-r--r--spec/frontend/boards/issue_card_inner_spec.js6
-rw-r--r--spec/frontend/boards/issue_spec.js2
-rw-r--r--spec/frontend/boards/list_spec.js3
-rw-r--r--spec/frontend/boards/mock_data.js6
-rw-r--r--spec/frontend/boards/project_select_deprecated_spec.js9
-rw-r--r--spec/frontend/boards/project_select_spec.js7
-rw-r--r--spec/frontend/boards/stores/actions_spec.js219
-rw-r--r--spec/frontend/boards/stores/getters_spec.js20
-rw-r--r--spec/frontend/boards/stores/mutations_spec.js44
-rw-r--r--spec/frontend/branches/divergence_graph_spec.js2
-rw-r--r--spec/frontend/captcha/captcha_modal_spec.js171
-rw-r--r--spec/frontend/captcha/init_recaptcha_script_spec.js59
-rw-r--r--spec/frontend/ci_lint/components/ci_lint_spec.js2
-rw-r--r--spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js4
-rw-r--r--spec/frontend/ci_variable_list/components/ci_environments_dropdown_spec.js4
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js6
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_popover_spec.js2
-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_table_spec.js4
-rw-r--r--spec/frontend/ci_variable_list/store/actions_spec.js8
-rw-r--r--spec/frontend/ci_variable_list/store/mutations_spec.js4
-rw-r--r--spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap8
-rw-r--r--spec/frontend/clusters/components/application_row_spec.js6
-rw-r--r--spec/frontend/clusters/components/applications_spec.js68
-rw-r--r--spec/frontend/clusters/components/fluentd_output_settings_spec.js2
-rw-r--r--spec/frontend/clusters/components/ingress_modsecurity_settings_spec.js2
-rw-r--r--spec/frontend/clusters/components/knative_domain_editor_spec.js2
-rw-r--r--spec/frontend/clusters/components/new_cluster_spec.js2
-rw-r--r--spec/frontend/clusters/components/remove_cluster_confirmation_spec.js4
-rw-r--r--spec/frontend/clusters/components/uninstall_application_button_spec.js2
-rw-r--r--spec/frontend/clusters/components/uninstall_application_confirmation_modal_spec.js2
-rw-r--r--spec/frontend/clusters/components/update_application_confirmation_modal_spec.js2
-rw-r--r--spec/frontend/clusters/forms/components/integration_form_spec.js4
-rw-r--r--spec/frontend/clusters/services/application_state_machine_spec.js2
-rw-r--r--spec/frontend/clusters/services/crossplane_provider_stack_spec.js2
-rw-r--r--spec/frontend/clusters/stores/clusters_store_spec.js2
-rw-r--r--spec/frontend/clusters_list/components/ancestor_notice_spec.js2
-rw-r--r--spec/frontend/clusters_list/components/clusters_spec.js8
-rw-r--r--spec/frontend/clusters_list/components/node_error_help_text_spec.js2
-rw-r--r--spec/frontend/clusters_list/store/actions_spec.js10
-rw-r--r--spec/frontend/clusters_list/store/mutations_spec.js4
-rw-r--r--spec/frontend/code_navigation/components/app_spec.js4
-rw-r--r--spec/frontend/code_navigation/components/popover_spec.js2
-rw-r--r--spec/frontend/code_navigation/store/actions_spec.js2
-rw-r--r--spec/frontend/collapsed_sidebar_todo_spec.js16
-rw-r--r--spec/frontend/commit/commit_pipeline_status_component_spec.js10
-rw-r--r--spec/frontend/commit/pipelines/pipelines_spec.js4
-rw-r--r--spec/frontend/commits_spec.js4
-rw-r--r--spec/frontend/commons/nav/user_merge_requests_spec.js2
-rw-r--r--spec/frontend/confidential_merge_request/components/dropdown_spec.js2
-rw-r--r--spec/frontend/confidential_merge_request/components/project_form_group_spec.js2
-rw-r--r--spec/frontend/contributors/component/contributors_spec.js4
-rw-r--r--spec/frontend/contributors/store/actions_spec.js4
-rw-r--r--spec/frontend/contributors/store/mutations_spec.js4
-rw-r--r--spec/frontend/create_cluster/components/cluster_form_dropdown_spec.js2
-rw-r--r--spec/frontend/create_cluster/eks_cluster/components/create_eks_cluster_spec.js2
-rw-r--r--spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js4
-rw-r--r--spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js4
-rw-r--r--spec/frontend/create_cluster/eks_cluster/services/aws_services_facade_spec.js2
-rw-r--r--spec/frontend/create_cluster/eks_cluster/store/actions_spec.js10
-rw-r--r--spec/frontend/create_cluster/eks_cluster/store/mutations_spec.js2
-rw-r--r--spec/frontend/create_cluster/gke_cluster/components/gke_machine_type_dropdown_spec.js4
-rw-r--r--spec/frontend/create_cluster/gke_cluster/components/gke_network_dropdown_spec.js4
-rw-r--r--spec/frontend/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js4
-rw-r--r--spec/frontend/create_cluster/gke_cluster/components/gke_submit_button_spec.js2
-rw-r--r--spec/frontend/create_cluster/gke_cluster/components/gke_subnetwork_dropdown_spec.js4
-rw-r--r--spec/frontend/create_cluster/gke_cluster/components/gke_zone_dropdown_spec.js4
-rw-r--r--spec/frontend/create_cluster/gke_cluster/stores/actions_spec.js4
-rw-r--r--spec/frontend/create_cluster/gke_cluster/stores/mutations_spec.js2
-rw-r--r--spec/frontend/create_cluster/init_create_cluster_spec.js2
-rw-r--r--spec/frontend/create_cluster/store/cluster_dropdown/actions_spec.js4
-rw-r--r--spec/frontend/create_cluster/store/cluster_dropdown/mutations_spec.js2
-rw-r--r--spec/frontend/create_merge_request_dropdown_spec.js4
-rw-r--r--spec/frontend/cycle_analytics/limit_warning_component_spec.js4
-rw-r--r--spec/frontend/deploy_freeze/components/deploy_freeze_modal_spec.js6
-rw-r--r--spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js4
-rw-r--r--spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js2
-rw-r--r--spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js6
-rw-r--r--spec/frontend/deploy_freeze/store/actions_spec.js6
-rw-r--r--spec/frontend/deploy_freeze/store/mutations_spec.js4
-rw-r--r--spec/frontend/deploy_keys/components/action_btn_spec.js4
-rw-r--r--spec/frontend/deploy_keys/components/app_spec.js6
-rw-r--r--spec/frontend/deploy_keys/components/key_spec.js4
-rw-r--r--spec/frontend/deploy_keys/components/keys_panel_spec.js2
-rw-r--r--spec/frontend/design_management/components/delete_button_spec.js2
-rw-r--r--spec/frontend/design_management/components/design_notes/design_discussion_spec.js6
-rw-r--r--spec/frontend/design_management/components/design_notes/design_note_spec.js4
-rw-r--r--spec/frontend/design_management/components/design_notes/toggle_replies_widget_spec.js4
-rw-r--r--spec/frontend/design_management/components/design_overlay_spec.js2
-rw-r--r--spec/frontend/design_management/components/design_presentation_spec.js4
-rw-r--r--spec/frontend/design_management/components/design_scaler_spec.js2
-rw-r--r--spec/frontend/design_management/components/design_sidebar_spec.js8
-rw-r--r--spec/frontend/design_management/components/design_todo_button_spec.js4
-rw-r--r--spec/frontend/design_management/components/image_spec.js2
-rw-r--r--spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap10
-rw-r--r--spec/frontend/design_management/components/list/item_spec.js52
-rw-r--r--spec/frontend/design_management/components/toolbar/index_spec.js4
-rw-r--r--spec/frontend/design_management/components/upload/button_spec.js2
-rw-r--r--spec/frontend/design_management/components/upload/design_version_dropdown_spec.js2
-rw-r--r--spec/frontend/design_management/pages/design/index_spec.js26
-rw-r--r--spec/frontend/design_management/pages/index_spec.js30
-rw-r--r--spec/frontend/design_management/router_spec.js2
-rw-r--r--spec/frontend/design_management/utils/cache_update_spec.js2
-rw-r--r--spec/frontend/design_management/utils/design_management_utils_spec.js4
-rw-r--r--spec/frontend/diffs/components/app_spec.js24
-rw-r--r--spec/frontend/diffs/components/collapsed_files_warning_spec.js4
-rw-r--r--spec/frontend/diffs/components/commit_item_spec.js2
-rw-r--r--spec/frontend/diffs/components/commit_widget_spec.js2
-rw-r--r--spec/frontend/diffs/components/compare_dropdown_layout_spec.js4
-rw-r--r--spec/frontend/diffs/components/compare_versions_spec.js4
-rw-r--r--spec/frontend/diffs/components/diff_comment_cell_spec.js2
-rw-r--r--spec/frontend/diffs/components/diff_content_spec.js14
-rw-r--r--spec/frontend/diffs/components/diff_discussions_spec.js8
-rw-r--r--spec/frontend/diffs/components/diff_expansion_cell_spec.js8
-rw-r--r--spec/frontend/diffs/components/diff_file_header_spec.js304
-rw-r--r--spec/frontend/diffs/components/diff_file_row_spec.js2
-rw-r--r--spec/frontend/diffs/components/diff_file_spec.js78
-rw-r--r--spec/frontend/diffs/components/diff_line_note_form_spec.js5
-rw-r--r--spec/frontend/diffs/components/diff_row_spec.js6
-rw-r--r--spec/frontend/diffs/components/diff_row_utils_spec.js11
-rw-r--r--spec/frontend/diffs/components/diff_stats_spec.js2
-rw-r--r--spec/frontend/diffs/components/diff_view_spec.js14
-rw-r--r--spec/frontend/diffs/components/image_diff_overlay_spec.js2
-rw-r--r--spec/frontend/diffs/components/inline_diff_table_row_spec.js8
-rw-r--r--spec/frontend/diffs/components/inline_diff_view_spec.js8
-rw-r--r--spec/frontend/diffs/components/no_changes_spec.js4
-rw-r--r--spec/frontend/diffs/components/parallel_diff_table_row_spec.js10
-rw-r--r--spec/frontend/diffs/components/parallel_diff_view_spec.js6
-rw-r--r--spec/frontend/diffs/components/settings_dropdown_spec.js2
-rw-r--r--spec/frontend/diffs/components/tree_list_spec.js2
-rw-r--r--spec/frontend/diffs/store/actions_spec.js8
-rw-r--r--spec/frontend/diffs/store/getters_spec.js68
-rw-r--r--spec/frontend/diffs/store/getters_versions_dropdowns_spec.js4
-rw-r--r--spec/frontend/diffs/store/mutations_spec.js6
-rw-r--r--spec/frontend/diffs/store/utils_spec.js4
-rw-r--r--spec/frontend/diffs/utils/diff_file_spec.js13
-rw-r--r--spec/frontend/diffs/utils/file_reviews_spec.js48
-rw-r--r--spec/frontend/diffs/utils/preferences_spec.js5
-rw-r--r--spec/frontend/diffs/utils/suggestions_spec.js15
-rw-r--r--spec/frontend/dirty_submit/dirty_submit_factory_spec.js2
-rw-r--r--spec/frontend/droplab/drop_down_spec.js2
-rw-r--r--spec/frontend/droplab/hook_spec.js2
-rw-r--r--spec/frontend/droplab/plugins/ajax_filter_spec.js2
-rw-r--r--spec/frontend/droplab/plugins/ajax_spec.js2
-rw-r--r--spec/frontend/dropzone_input_spec.js4
-rw-r--r--spec/frontend/editor/editor_ci_schema_ext_spec.js2
-rw-r--r--spec/frontend/editor/editor_lite_spec.js298
-rw-r--r--spec/frontend/emoji/index_spec.js (renamed from spec/frontend/emoji/emoji_spec.js)361
-rw-r--r--spec/frontend/environment.js13
-rw-r--r--spec/frontend/environments/canary_ingress_spec.js4
-rw-r--r--spec/frontend/environments/canary_update_modal_spec.js2
-rw-r--r--spec/frontend/environments/confirm_rollback_modal_spec.js2
-rw-r--r--spec/frontend/environments/deploy_board_component_spec.js2
-rw-r--r--spec/frontend/environments/enable_review_app_modal_spec.js2
-rw-r--r--spec/frontend/environments/environment_actions_spec.js4
-rw-r--r--spec/frontend/environments/environment_item_spec.js4
-rw-r--r--spec/frontend/environments/environment_monitoring_spec.js2
-rw-r--r--spec/frontend/environments/environment_pin_spec.js4
-rw-r--r--spec/frontend/environments/environment_rollback_spec.js4
-rw-r--r--spec/frontend/environments/environment_stop_spec.js4
-rw-r--r--spec/frontend/environments/environment_table_spec.js4
-rw-r--r--spec/frontend/environments/environments_app_spec.js4
-rw-r--r--spec/frontend/error_tracking/components/error_details_spec.js14
-rw-r--r--spec/frontend/error_tracking/components/error_tracking_actions_spec.js2
-rw-r--r--spec/frontend/error_tracking/components/error_tracking_list_spec.js10
-rw-r--r--spec/frontend/error_tracking/components/stacktrace_entry_spec.js2
-rw-r--r--spec/frontend/error_tracking/store/actions_spec.js4
-rw-r--r--spec/frontend/error_tracking/store/details/actions_spec.js4
-rw-r--r--spec/frontend/error_tracking/store/list/actions_spec.js6
-rw-r--r--spec/frontend/error_tracking/store/list/mutation_spec.js2
-rw-r--r--spec/frontend/error_tracking_settings/components/app_spec.js2
-rw-r--r--spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js4
-rw-r--r--spec/frontend/error_tracking_settings/components/project_dropdown_spec.js4
-rw-r--r--spec/frontend/error_tracking_settings/store/actions_spec.js8
-rw-r--r--spec/frontend/error_tracking_settings/store/mutation_spec.js2
-rw-r--r--spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js2
-rw-r--r--spec/frontend/feature_flags/components/edit_feature_flag_spec.js19
-rw-r--r--spec/frontend/feature_flags/components/environments_dropdown_spec.js6
-rw-r--r--spec/frontend/feature_flags/components/feature_flags_spec.js12
-rw-r--r--spec/frontend/feature_flags/components/feature_flags_tab_spec.js2
-rw-r--r--spec/frontend/feature_flags/components/feature_flags_table_spec.js4
-rw-r--r--spec/frontend/feature_flags/components/form_spec.js47
-rw-r--r--spec/frontend/feature_flags/components/new_environments_dropdown_spec.js2
-rw-r--r--spec/frontend/feature_flags/components/new_feature_flag_spec.js16
-rw-r--r--spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js2
-rw-r--r--spec/frontend/feature_flags/components/strategies/gitlab_user_list_spec.js4
-rw-r--r--spec/frontend/feature_flags/components/strategies/parameter_form_group_spec.js2
-rw-r--r--spec/frontend/feature_flags/components/strategies/percent_rollout_spec.js4
-rw-r--r--spec/frontend/feature_flags/components/strategies/users_with_id_spec.js2
-rw-r--r--spec/frontend/feature_flags/components/strategy_parameters_spec.js10
-rw-r--r--spec/frontend/feature_flags/components/strategy_spec.js12
-rw-r--r--spec/frontend/feature_flags/components/user_lists_table_spec.js2
-rw-r--r--spec/frontend/feature_flags/store/edit/actions_spec.js12
-rw-r--r--spec/frontend/feature_flags/store/edit/mutations_spec.js4
-rw-r--r--spec/frontend/feature_flags/store/gitlab_user_lists/actions_spec.js2
-rw-r--r--spec/frontend/feature_flags/store/gitlab_user_lists/getters_spec.js2
-rw-r--r--spec/frontend/feature_flags/store/gitlab_user_lists/mutations_spec.js4
-rw-r--r--spec/frontend/feature_flags/store/helpers_spec.js14
-rw-r--r--spec/frontend/feature_flags/store/index/actions_spec.js4
-rw-r--r--spec/frontend/feature_flags/store/index/mutations_spec.js6
-rw-r--r--spec/frontend/feature_flags/store/new/actions_spec.js16
-rw-r--r--spec/frontend/feature_flags/store/new/mutations_spec.js4
-rw-r--r--spec/frontend/feature_highlight/feature_highlight_helper_spec.js70
-rw-r--r--spec/frontend/feature_highlight/feature_highlight_options_spec.js22
-rw-r--r--spec/frontend/feature_highlight/feature_highlight_popover_spec.js80
-rw-r--r--spec/frontend/feature_highlight/feature_highlight_spec.js120
-rw-r--r--spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js2
-rw-r--r--spec/frontend/filtered_search/dropdown_user_spec.js3
-rw-r--r--spec/frontend/filtered_search/filtered_search_manager_spec.js12
-rw-r--r--spec/frontend/filtered_search/filtered_search_tokenizer_spec.js2
-rw-r--r--spec/frontend/filtered_search/recent_searches_root_spec.js49
-rw-r--r--spec/frontend/filtered_search/visual_token_value_spec.js4
-rw-r--r--spec/frontend/fixtures/api_merge_requests.rb19
-rw-r--r--spec/frontend/fixtures/merge_requests.rb1
-rw-r--r--spec/frontend/fixtures/merge_requests_diffs.rb2
-rw-r--r--spec/frontend/fixtures/pipelines.rb2
-rw-r--r--spec/frontend/frequent_items/components/app_spec.js39
-rw-r--r--spec/frontend/frequent_items/components/frequent_items_list_item_spec.js2
-rw-r--r--spec/frontend/frequent_items/components/frequent_items_list_spec.js2
-rw-r--r--spec/frontend/frequent_items/components/frequent_items_search_input_spec.js62
-rw-r--r--spec/frontend/frequent_items/store/actions_spec.js6
-rw-r--r--spec/frontend/frequent_items/store/getters_spec.js2
-rw-r--r--spec/frontend/frequent_items/store/mutations_spec.js4
-rw-r--r--spec/frontend/frequent_items/utils_spec.js2
-rw-r--r--spec/frontend/gfm_auto_complete_spec.js128
-rw-r--r--spec/frontend/gl_form_spec.js2
-rw-r--r--spec/frontend/gpg_badges_spec.js2
-rw-r--r--spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap6
-rw-r--r--spec/frontend/grafana_integration/components/grafana_integration_spec.js4
-rw-r--r--spec/frontend/group_settings/components/shared_runners_form_spec.js2
-rw-r--r--spec/frontend/groups/components/app_spec.js12
-rw-r--r--spec/frontend/groups/components/group_folder_spec.js2
-rw-r--r--spec/frontend/groups/components/group_item_spec.js6
-rw-r--r--spec/frontend/groups/components/groups_spec.js6
-rw-r--r--spec/frontend/groups/components/invite_members_banner_spec.js4
-rw-r--r--spec/frontend/groups/components/item_actions_spec.js16
-rw-r--r--spec/frontend/groups/components/item_caret_spec.js2
-rw-r--r--spec/frontend/groups/components/item_stats_value_spec.js2
-rw-r--r--spec/frontend/groups/components/item_type_icon_spec.js2
-rw-r--r--spec/frontend/groups/members/mock_data.js33
-rw-r--r--spec/frontend/groups/members/utils_spec.js45
-rw-r--r--spec/frontend/groups/service/groups_service_spec.js2
-rw-r--r--spec/frontend/ide/commit_icon_spec.js2
-rw-r--r--spec/frontend/ide/components/activity_bar_spec.js21
-rw-r--r--spec/frontend/ide/components/branches/item_spec.js6
-rw-r--r--spec/frontend/ide/components/branches/search_list_spec.js6
-rw-r--r--spec/frontend/ide/components/commit_sidebar/actions_spec.js27
-rw-r--r--spec/frontend/ide/components/commit_sidebar/editor_header_spec.js4
-rw-r--r--spec/frontend/ide/components/commit_sidebar/empty_state_spec.js2
-rw-r--r--spec/frontend/ide/components/commit_sidebar/form_spec.js395
-rw-r--r--spec/frontend/ide/components/commit_sidebar/list_item_spec.js2
-rw-r--r--spec/frontend/ide/components/commit_sidebar/list_spec.js2
-rw-r--r--spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js11
-rw-r--r--spec/frontend/ide/components/commit_sidebar/radio_group_spec.js2
-rw-r--r--spec/frontend/ide/components/commit_sidebar/success_message_spec.js2
-rw-r--r--spec/frontend/ide/components/error_message_spec.js2
-rw-r--r--spec/frontend/ide/components/file_row_extra_spec.js2
-rw-r--r--spec/frontend/ide/components/file_templates/bar_spec.js2
-rw-r--r--spec/frontend/ide/components/file_templates/dropdown_spec.js4
-rw-r--r--spec/frontend/ide/components/ide_file_row_spec.js4
-rw-r--r--spec/frontend/ide/components/ide_review_spec.js6
-rw-r--r--spec/frontend/ide/components/ide_side_bar_spec.js6
-rw-r--r--spec/frontend/ide/components/ide_sidebar_nav_spec.js5
-rw-r--r--spec/frontend/ide/components/ide_spec.js60
-rw-r--r--spec/frontend/ide/components/ide_status_bar_spec.js6
-rw-r--r--spec/frontend/ide/components/ide_status_list_spec.js4
-rw-r--r--spec/frontend/ide/components/ide_status_mr_spec.js2
-rw-r--r--spec/frontend/ide/components/ide_tree_spec.js2
-rw-r--r--spec/frontend/ide/components/jobs/detail/scroll_button_spec.js2
-rw-r--r--spec/frontend/ide/components/jobs/list_spec.js2
-rw-r--r--spec/frontend/ide/components/jobs/stage_spec.js4
-rw-r--r--spec/frontend/ide/components/merge_requests/item_spec.js6
-rw-r--r--spec/frontend/ide/components/merge_requests/list_spec.js4
-rw-r--r--spec/frontend/ide/components/nav_dropdown_spec.js4
-rw-r--r--spec/frontend/ide/components/new_dropdown/index_spec.js2
-rw-r--r--spec/frontend/ide/components/new_dropdown/modal_spec.js4
-rw-r--r--spec/frontend/ide/components/panes/collapsible_sidebar_spec.js4
-rw-r--r--spec/frontend/ide/components/panes/right_spec.js6
-rw-r--r--spec/frontend/ide/components/pipelines/list_spec.js6
-rw-r--r--spec/frontend/ide/components/preview/clientside_spec.js2
-rw-r--r--spec/frontend/ide/components/preview/navigator_spec.js4
-rw-r--r--spec/frontend/ide/components/repo_commit_section_spec.js6
-rw-r--r--spec/frontend/ide/components/repo_editor_spec.js16
-rw-r--r--spec/frontend/ide/components/repo_tab_spec.js2
-rw-r--r--spec/frontend/ide/components/repo_tabs_spec.js4
-rw-r--r--spec/frontend/ide/components/resizable_panel_spec.js4
-rw-r--r--spec/frontend/ide/components/terminal/empty_state_spec.js2
-rw-r--r--spec/frontend/ide/components/terminal/session_spec.js2
-rw-r--r--spec/frontend/ide/components/terminal/terminal_controls_spec.js2
-rw-r--r--spec/frontend/ide/components/terminal/terminal_spec.js4
-rw-r--r--spec/frontend/ide/components/terminal/view_spec.js4
-rw-r--r--spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js2
-rw-r--r--spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js4
-rw-r--r--spec/frontend/ide/helpers.js2
-rw-r--r--spec/frontend/ide/lib/create_diff_spec.js2
-rw-r--r--spec/frontend/ide/lib/create_file_diff_spec.js2
-rw-r--r--spec/frontend/ide/lib/decorations/controller_spec.js6
-rw-r--r--spec/frontend/ide/lib/diff/controller_spec.js2
-rw-r--r--spec/frontend/ide/lib/editor_spec.js5
-rw-r--r--spec/frontend/ide/lib/languages/hcl_spec.js2
-rw-r--r--spec/frontend/ide/lib/languages/vue_spec.js2
-rw-r--r--spec/frontend/ide/services/index_spec.js4
-rw-r--r--spec/frontend/ide/stores/actions/file_spec.js10
-rw-r--r--spec/frontend/ide/stores/actions/merge_request_spec.js111
-rw-r--r--spec/frontend/ide/stores/actions/project_spec.js8
-rw-r--r--spec/frontend/ide/stores/actions/tree_spec.js8
-rw-r--r--spec/frontend/ide/stores/actions_spec.js8
-rw-r--r--spec/frontend/ide/stores/getters_spec.js7
-rw-r--r--spec/frontend/ide/stores/modules/branches/actions_spec.js6
-rw-r--r--spec/frontend/ide/stores/modules/branches/mutations_spec.js4
-rw-r--r--spec/frontend/ide/stores/modules/clientside/actions_spec.js4
-rw-r--r--spec/frontend/ide/stores/modules/commit/actions_spec.js31
-rw-r--r--spec/frontend/ide/stores/modules/commit/getters_spec.js11
-rw-r--r--spec/frontend/ide/stores/modules/commit/mutations_spec.js4
-rw-r--r--spec/frontend/ide/stores/modules/editor/actions_spec.js2
-rw-r--r--spec/frontend/ide/stores/modules/editor/getters_spec.js2
-rw-r--r--spec/frontend/ide/stores/modules/editor/mutations_spec.js2
-rw-r--r--spec/frontend/ide/stores/modules/file_templates/actions_spec.js4
-rw-r--r--spec/frontend/ide/stores/modules/file_templates/getters_spec.js2
-rw-r--r--spec/frontend/ide/stores/modules/file_templates/mutations_spec.js2
-rw-r--r--spec/frontend/ide/stores/modules/merge_requests/actions_spec.js6
-rw-r--r--spec/frontend/ide/stores/modules/merge_requests/mutations_spec.js4
-rw-r--r--spec/frontend/ide/stores/modules/pane/mutations_spec.js4
-rw-r--r--spec/frontend/ide/stores/modules/pipelines/actions_spec.js8
-rw-r--r--spec/frontend/ide/stores/modules/pipelines/mutations_spec.js2
-rw-r--r--spec/frontend/ide/stores/modules/router/mutations_spec.js2
-rw-r--r--spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js4
-rw-r--r--spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js6
-rw-r--r--spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js4
-rw-r--r--spec/frontend/ide/stores/modules/terminal/actions/setup_spec.js2
-rw-r--r--spec/frontend/ide/stores/modules/terminal/messages_spec.js2
-rw-r--r--spec/frontend/ide/stores/modules/terminal/mutations_spec.js2
-rw-r--r--spec/frontend/ide/stores/modules/terminal_sync/actions_spec.js2
-rw-r--r--spec/frontend/ide/stores/modules/terminal_sync/mutations_spec.js2
-rw-r--r--spec/frontend/ide/stores/mutations/file_spec.js2
-rw-r--r--spec/frontend/ide/stores/plugins/terminal_spec.js2
-rw-r--r--spec/frontend/ide/stores/plugins/terminal_sync_spec.js8
-rw-r--r--spec/frontend/ide/stores/utils_spec.js2
-rw-r--r--spec/frontend/image_diff/image_badge_spec.js2
-rw-r--r--spec/frontend/image_diff/image_diff_spec.js2
-rw-r--r--spec/frontend/image_diff/init_discussion_tab_spec.js2
-rw-r--r--spec/frontend/image_diff/replaced_image_diff_spec.js4
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_row_spec.js6
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_spec.js167
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js137
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js4
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js276
-rw-r--r--spec/frontend/import_entities/import_projects/components/bitbucket_status_table_spec.js4
-rw-r--r--spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js14
-rw-r--r--spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js12
-rw-r--r--spec/frontend/import_entities/import_projects/store/actions_spec.js12
-rw-r--r--spec/frontend/import_entities/import_projects/store/getters_spec.js2
-rw-r--r--spec/frontend/import_entities/import_projects/store/mutations_spec.js2
-rw-r--r--spec/frontend/import_entities/import_projects/utils_spec.js2
-rw-r--r--spec/frontend/incidents/components/incidents_list_spec.js10
-rw-r--r--spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap4
-rw-r--r--spec/frontend/incidents_settings/components/incidents_settings_service_spec.js6
-rw-r--r--spec/frontend/incidents_settings/components/incidents_settings_tabs_spec.js2
-rw-r--r--spec/frontend/incidents_settings/components/pagerduty_form_spec.js2
-rw-r--r--spec/frontend/integrations/edit/components/active_checkbox_spec.js5
-rw-r--r--spec/frontend/integrations/edit/components/confirmation_modal_spec.js5
-rw-r--r--spec/frontend/integrations/edit/components/dynamic_field_spec.js2
-rw-r--r--spec/frontend/integrations/edit/components/integration_form_spec.js169
-rw-r--r--spec/frontend/integrations/edit/components/jira_issues_fields_spec.js86
-rw-r--r--spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js2
-rw-r--r--spec/frontend/integrations/edit/components/override_dropdown_spec.js7
-rw-r--r--spec/frontend/integrations/edit/components/trigger_fields_spec.js2
-rw-r--r--spec/frontend/integrations/edit/store/actions_spec.js37
-rw-r--r--spec/frontend/integrations/edit/store/getters_spec.js4
-rw-r--r--spec/frontend/integrations/edit/store/mutations_spec.js28
-rw-r--r--spec/frontend/integrations/edit/store/state_spec.js3
-rw-r--r--spec/frontend/integrations/integration_settings_form_spec.js81
-rw-r--r--spec/frontend/invite_member/components/invite_member_modal_spec.js4
-rw-r--r--spec/frontend/invite_member/components/invite_member_trigger_spec.js2
-rw-r--r--spec/frontend/invite_members/components/invite_members_modal_spec.js2
-rw-r--r--spec/frontend/invite_members/components/invite_members_trigger_spec.js2
-rw-r--r--spec/frontend/invite_members/components/members_token_select_spec.js4
-rw-r--r--spec/frontend/issuable/components/issuable_by_email_spec.js164
-rw-r--r--spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js2
-rw-r--r--spec/frontend/issuable/related_issues/components/issue_token_spec.js2
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_block_spec.js2
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_root_spec.js6
-rw-r--r--spec/frontend/issuable_create/components/issuable_form_spec.js5
-rw-r--r--spec/frontend/issuable_list/components/issuable_item_spec.js70
-rw-r--r--spec/frontend/issuable_list/components/issuable_list_root_spec.js4
-rw-r--r--spec/frontend/issuable_list/components/issuable_tabs_spec.js2
-rw-r--r--spec/frontend/issuable_show/components/issuable_body_spec.js11
-rw-r--r--spec/frontend/issuable_show/components/issuable_description_spec.js2
-rw-r--r--spec/frontend/issuable_show/components/issuable_edit_form_spec.js4
-rw-r--r--spec/frontend/issuable_show/components/issuable_header_spec.js61
-rw-r--r--spec/frontend/issuable_show/components/issuable_show_root_spec.js4
-rw-r--r--spec/frontend/issuable_show/components/issuable_title_spec.js2
-rw-r--r--spec/frontend/issuable_sidebar/components/issuable_sidebar_root_spec.js2
-rw-r--r--spec/frontend/issuable_spec.js42
-rw-r--r--spec/frontend/issuable_suggestions/components/item_spec.js4
-rw-r--r--spec/frontend/issue_show/components/app_spec.js14
-rw-r--r--spec/frontend/issue_show/components/description_spec.js2
-rw-r--r--spec/frontend/issue_show/components/fields/description_spec.js78
-rw-r--r--spec/frontend/issue_show/components/fields/description_template_spec.js6
-rw-r--r--spec/frontend/issue_show/components/fields/title_spec.js48
-rw-r--r--spec/frontend/issue_show/components/form_spec.js9
-rw-r--r--spec/frontend/issue_show/components/incidents/highlight_bar_spec.js2
-rw-r--r--spec/frontend/issue_show/components/incidents/incident_tabs_spec.js12
-rw-r--r--spec/frontend/issue_show/components/pinned_links_spec.js2
-rw-r--r--spec/frontend/issue_show/components/title_spec.js2
-rw-r--r--spec/frontend/issue_show/helpers.js9
-rw-r--r--spec/frontend/issue_show/issue_spec.js4
-rw-r--r--spec/frontend/issue_show/mock_data.js1
-rw-r--r--spec/frontend/issue_spec.js4
-rw-r--r--spec/frontend/issues_list/components/issuable_spec.js37
-rw-r--r--spec/frontend/issues_list/components/issuables_list_app_spec.js84
-rw-r--r--spec/frontend/jira_connect/api_spec.js5
-rw-r--r--spec/frontend/jira_connect/components/app_spec.js36
-rw-r--r--spec/frontend/jira_connect/components/groups_list_item_spec.js109
-rw-r--r--spec/frontend/jira_connect/components/groups_list_spec.js30
-rw-r--r--spec/frontend/jira_connect/index_spec.js56
-rw-r--r--spec/frontend/jira_connect/mock_data.js2
-rw-r--r--spec/frontend/jira_import/components/jira_import_form_spec.js81
-rw-r--r--spec/frontend/jira_import/mock_data.js2
-rw-r--r--spec/frontend/jira_import/utils/jira_import_utils_spec.js2
-rw-r--r--spec/frontend/jobs/components/artifacts_block_spec.js2
-rw-r--r--spec/frontend/jobs/components/erased_block_spec.js8
-rw-r--r--spec/frontend/jobs/components/job_app_spec.js13
-rw-r--r--spec/frontend/jobs/components/job_container_item_spec.js102
-rw-r--r--spec/frontend/jobs/components/job_sidebar_details_container_spec.js11
-rw-r--r--spec/frontend/jobs/components/job_sidebar_retry_button_spec.js2
-rw-r--r--spec/frontend/jobs/components/log/line_header_spec.js2
-rw-r--r--spec/frontend/jobs/components/log/log_spec.js2
-rw-r--r--spec/frontend/jobs/components/manual_variables_form_spec.js2
-rw-r--r--spec/frontend/jobs/components/sidebar_detail_row_spec.js72
-rw-r--r--spec/frontend/jobs/components/sidebar_spec.js6
-rw-r--r--spec/frontend/jobs/components/trigger_block_spec.js116
-rw-r--r--spec/frontend/jobs/components/unmet_prerequisites_block_spec.js2
-rw-r--r--spec/frontend/jobs/mixins/delayed_job_mixin_spec.js98
-rw-r--r--spec/frontend/jobs/store/actions_spec.js6
-rw-r--r--spec/frontend/jobs/store/mutations_spec.js4
-rw-r--r--spec/frontend/lazy_loader_spec.js2
-rw-r--r--spec/frontend/lib/utils/ajax_cache_spec.js2
-rw-r--r--spec/frontend/lib/utils/array_utility_spec.js32
-rw-r--r--spec/frontend/lib/utils/color_utils_spec.js17
-rw-r--r--spec/frontend/lib/utils/common_utils_spec.js8
-rw-r--r--spec/frontend/lib/utils/datetime_utility_spec.js345
-rw-r--r--spec/frontend/lib/utils/poll_spec.js2
-rw-r--r--spec/frontend/lib/utils/poll_until_complete_spec.js2
-rw-r--r--spec/frontend/lib/utils/unit_format/formatter_factory_spec.js21
-rw-r--r--spec/frontend/lib/utils/url_utility_spec.js55
-rw-r--r--spec/frontend/line_highlighter_spec.js2
-rw-r--r--spec/frontend/logs/components/environment_logs_spec.js2
-rw-r--r--spec/frontend/logs/components/log_advanced_filters_spec.js9
-rw-r--r--spec/frontend/logs/components/log_control_buttons_spec.js2
-rw-r--r--spec/frontend/logs/components/log_simple_filters_spec.js3
-rw-r--r--spec/frontend/logs/stores/actions_spec.js13
-rw-r--r--spec/frontend/logs/stores/mutations_spec.js2
-rw-r--r--spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js2
-rw-r--r--spec/frontend/members/components/action_buttons/approve_access_request_button_spec.js2
-rw-r--r--spec/frontend/members/components/action_buttons/leave_button_spec.js2
-rw-r--r--spec/frontend/members/components/action_buttons/remove_group_link_button_spec.js2
-rw-r--r--spec/frontend/members/components/action_buttons/resend_invite_button_spec.js2
-rw-r--r--spec/frontend/members/components/action_buttons/user_action_buttons_spec.js4
-rw-r--r--spec/frontend/members/components/app_spec.js (renamed from spec/frontend/groups/members/components/app_spec.js)12
-rw-r--r--spec/frontend/members/components/avatars/group_avatar_spec.js6
-rw-r--r--spec/frontend/members/components/avatars/invite_avatar_spec.js4
-rw-r--r--spec/frontend/members/components/avatars/user_avatar_spec.js6
-rw-r--r--spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js2
-rw-r--r--spec/frontend/members/components/filter_sort/sort_dropdown_spec.js4
-rw-r--r--spec/frontend/members/components/modals/leave_modal_spec.js4
-rw-r--r--spec/frontend/members/components/modals/remove_group_link_modal_spec.js4
-rw-r--r--spec/frontend/members/components/table/created_at_spec.js2
-rw-r--r--spec/frontend/members/components/table/expiration_datepicker_spec.js4
-rw-r--r--spec/frontend/members/components/table/expires_at_spec.js2
-rw-r--r--spec/frontend/members/components/table/member_action_buttons_spec.js10
-rw-r--r--spec/frontend/members/components/table/member_avatar_spec.js8
-rw-r--r--spec/frontend/members/components/table/member_source_spec.js2
-rw-r--r--spec/frontend/members/components/table/members_table_cell_spec.js24
-rw-r--r--spec/frontend/members/components/table/members_table_spec.js25
-rw-r--r--spec/frontend/members/components/table/role_dropdown_spec.js11
-rw-r--r--spec/frontend/members/index_spec.js (renamed from spec/frontend/groups/members/index_spec.js)20
-rw-r--r--spec/frontend/members/mock_data.js6
-rw-r--r--spec/frontend/members/store/actions_spec.js20
-rw-r--r--spec/frontend/members/store/mutations_spec.js66
-rw-r--r--spec/frontend/members/utils_spec.js100
-rw-r--r--spec/frontend/merge_request/components/status_box_spec.js10
-rw-r--r--spec/frontend/merge_request_spec.js2
-rw-r--r--spec/frontend/merge_request_tabs_spec.js3
-rw-r--r--spec/frontend/milestones/milestone_combobox_spec.js8
-rw-r--r--spec/frontend/milestones/stores/actions_spec.js2
-rw-r--r--spec/frontend/milestones/stores/mutations_spec.js4
-rw-r--r--spec/frontend/mini_pipeline_graph_dropdown_spec.js2
-rw-r--r--spec/frontend/monitoring/alert_widget_spec.js4
-rw-r--r--spec/frontend/monitoring/components/alert_widget_form_spec.js2
-rw-r--r--spec/frontend/monitoring/components/charts/anomaly_spec.js4
-rw-r--r--spec/frontend/monitoring/components/charts/bar_spec.js2
-rw-r--r--spec/frontend/monitoring/components/charts/column_spec.js2
-rw-r--r--spec/frontend/monitoring/components/charts/gauge_spec.js2
-rw-r--r--spec/frontend/monitoring/components/charts/heatmap_spec.js2
-rw-r--r--spec/frontend/monitoring/components/charts/single_stat_spec.js13
-rw-r--r--spec/frontend/monitoring/components/charts/stacked_column_spec.js4
-rw-r--r--spec/frontend/monitoring/components/charts/time_series_spec.js13
-rw-r--r--spec/frontend/monitoring/components/create_dashboard_modal_spec.js2
-rw-r--r--spec/frontend/monitoring/components/dashboard_actions_menu_spec.js14
-rw-r--r--spec/frontend/monitoring/components/dashboard_header_spec.js14
-rw-r--r--spec/frontend/monitoring/components/dashboard_panel_builder_spec.js9
-rw-r--r--spec/frontend/monitoring/components/dashboard_panel_spec.js46
-rw-r--r--spec/frontend/monitoring/components/dashboard_spec.js24
-rw-r--r--spec/frontend/monitoring/components/dashboard_template_spec.js2
-rw-r--r--spec/frontend/monitoring/components/dashboard_url_time_spec.js6
-rw-r--r--spec/frontend/monitoring/components/dashboards_dropdown_spec.js4
-rw-r--r--spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js2
-rw-r--r--spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js8
-rw-r--r--spec/frontend/monitoring/components/embeds/embed_group_spec.js2
-rw-r--r--spec/frontend/monitoring/components/embeds/metric_embed_spec.js2
-rw-r--r--spec/frontend/monitoring/components/empty_state_spec.js4
-rw-r--r--spec/frontend/monitoring/components/graph_group_spec.js2
-rw-r--r--spec/frontend/monitoring/components/links_section_spec.js4
-rw-r--r--spec/frontend/monitoring/components/refresh_button_spec.js4
-rw-r--r--spec/frontend/monitoring/components/variables/dropdown_field_spec.js2
-rw-r--r--spec/frontend/monitoring/components/variables/text_field_spec.js2
-rw-r--r--spec/frontend/monitoring/components/variables_section_spec.js4
-rw-r--r--spec/frontend/monitoring/csv_export_spec.js2
-rw-r--r--spec/frontend/monitoring/fixture_data.js6
-rw-r--r--spec/frontend/monitoring/graph_data.js2
-rw-r--r--spec/frontend/monitoring/mock_data.js2
-rw-r--r--spec/frontend/monitoring/pages/dashboard_page_spec.js4
-rw-r--r--spec/frontend/monitoring/pages/panel_new_page_spec.js7
-rw-r--r--spec/frontend/monitoring/requests/index_spec.js4
-rw-r--r--spec/frontend/monitoring/router_spec.js4
-rw-r--r--spec/frontend/monitoring/store/actions_spec.js32
-rw-r--r--spec/frontend/monitoring/store/embed_group/mutations_spec.js4
-rw-r--r--spec/frontend/monitoring/store/getters_spec.js6
-rw-r--r--spec/frontend/monitoring/store/mutations_spec.js8
-rw-r--r--spec/frontend/monitoring/store/utils_spec.js4
-rw-r--r--spec/frontend/monitoring/store/variable_mapping_spec.js2
-rw-r--r--spec/frontend/monitoring/store_utils.js2
-rw-r--r--spec/frontend/monitoring/utils_spec.js6
-rw-r--r--spec/frontend/namespace_select_spec.js2
-rw-r--r--spec/frontend/notebook/cells/markdown_spec.js2
-rw-r--r--spec/frontend/notes/components/comment_form_spec.js143
-rw-r--r--spec/frontend/notes/components/diff_discussion_header_spec.js4
-rw-r--r--spec/frontend/notes/components/diff_with_note_spec.js2
-rw-r--r--spec/frontend/notes/components/discussion_actions_spec.js2
-rw-r--r--spec/frontend/notes/components/discussion_counter_spec.js8
-rw-r--r--spec/frontend/notes/components/discussion_filter_note_spec.js2
-rw-r--r--spec/frontend/notes/components/discussion_filter_spec.js4
-rw-r--r--spec/frontend/notes/components/discussion_navigator_spec.js2
-rw-r--r--spec/frontend/notes/components/discussion_notes_spec.js27
-rw-r--r--spec/frontend/notes/components/discussion_resolve_button_spec.js2
-rw-r--r--spec/frontend/notes/components/multiline_comment_form_spec.js4
-rw-r--r--spec/frontend/notes/components/note_actions/reply_button_spec.js19
-rw-r--r--spec/frontend/notes/components/note_actions_spec.js11
-rw-r--r--spec/frontend/notes/components/note_awards_list_spec.js4
-rw-r--r--spec/frontend/notes/components/note_body_spec.js56
-rw-r--r--spec/frontend/notes/components/note_form_spec.js32
-rw-r--r--spec/frontend/notes/components/note_header_spec.js11
-rw-r--r--spec/frontend/notes/components/noteable_discussion_spec.js8
-rw-r--r--spec/frontend/notes/components/noteable_note_spec.js19
-rw-r--r--spec/frontend/notes/components/notes_app_spec.js12
-rw-r--r--spec/frontend/notes/components/sort_discussion_spec.js4
-rw-r--r--spec/frontend/notes/components/timeline_toggle_spec.js4
-rw-r--r--spec/frontend/notes/mixins/discussion_navigation_spec.js6
-rw-r--r--spec/frontend/notes/old_notes_spec.js6
-rw-r--r--spec/frontend/notes/stores/actions_spec.js84
-rw-r--r--spec/frontend/notes/stores/getters_spec.js2
-rw-r--r--spec/frontend/notes/stores/mutation_spec.js15
-rw-r--r--spec/frontend/notifications/components/custom_notifications_modal_spec.js267
-rw-r--r--spec/frontend/notifications/components/notifications_dropdown_spec.js274
-rw-r--r--spec/frontend/onboarding_issues/index_spec.js137
-rw-r--r--spec/frontend/operation_settings/components/metrics_settings_spec.js14
-rw-r--r--spec/frontend/operation_settings/store/mutations_spec.js2
-rw-r--r--spec/frontend/packages/details/components/additional_metadata_spec.js4
-rw-r--r--spec/frontend/packages/details/components/app_spec.js25
-rw-r--r--spec/frontend/packages/details/components/composer_installation_spec.js4
-rw-r--r--spec/frontend/packages/details/components/conan_installation_spec.js2
-rw-r--r--spec/frontend/packages/details/components/installations_commands_spec.js6
-rw-r--r--spec/frontend/packages/details/components/maven_installation_spec.js4
-rw-r--r--spec/frontend/packages/details/components/npm_installation_spec.js6
-rw-r--r--spec/frontend/packages/details/components/nuget_installation_spec.js6
-rw-r--r--spec/frontend/packages/details/components/package_files_spec.js4
-rw-r--r--spec/frontend/packages/details/components/package_history_spec.js8
-rw-r--r--spec/frontend/packages/details/components/package_title_spec.js4
-rw-r--r--spec/frontend/packages/details/components/pypi_installation_spec.js2
-rw-r--r--spec/frontend/packages/details/store/actions_spec.js2
-rw-r--r--spec/frontend/packages/details/store/getters_spec.js2
-rw-r--r--spec/frontend/packages/details/store/mutations_spec.js2
-rw-r--r--spec/frontend/packages/list/components/__snapshots__/packages_filter_spec.js.snap14
-rw-r--r--spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap555
-rw-r--r--spec/frontend/packages/list/components/packages_filter_spec.js50
-rw-r--r--spec/frontend/packages/list/components/packages_list_app_spec.js65
-rw-r--r--spec/frontend/packages/list/components/packages_list_spec.js10
-rw-r--r--spec/frontend/packages/list/components/packages_search_spec.js107
-rw-r--r--spec/frontend/packages/list/components/packages_sort_spec.js90
-rw-r--r--spec/frontend/packages/list/components/packages_title_spec.js4
-rw-r--r--spec/frontend/packages/list/components/tokens/package_type_token_spec.js48
-rw-r--r--spec/frontend/packages/list/stores/actions_spec.js14
-rw-r--r--spec/frontend/packages/list/stores/mutations_spec.js13
-rw-r--r--spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap10
-rw-r--r--spec/frontend/packages/shared/components/package_list_row_spec.js2
-rw-r--r--spec/frontend/packages/shared/components/packages_list_loader_spec.js4
-rw-r--r--spec/frontend/packages/shared/utils_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js309
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/maven_settings_spec.js153
-rw-r--r--spec/frontend/packages_and_registries/settings/group/graphl/utils/cache_update_spec.js56
-rw-r--r--spec/frontend/packages_and_registries/settings/group/graphl/utils/optimistic_responses_spec.js20
-rw-r--r--spec/frontend/packages_and_registries/settings/group/mock_data.js48
-rw-r--r--spec/frontend/pager_spec.js4
-rw-r--r--spec/frontend/pages/admin/jobs/index/components/stop_jobs_modal_spec.js4
-rw-r--r--spec/frontend/pages/admin/users/components/delete_user_modal_spec.js2
-rw-r--r--spec/frontend/pages/admin/users/components/user_modal_manager_spec.js97
-rw-r--r--spec/frontend/pages/dashboard/projects/index/components/customize_homepage_banner_spec.js4
-rw-r--r--spec/frontend/pages/dashboard/todos/index/todos_spec.js4
-rw-r--r--spec/frontend/pages/import/bitbucket_server/components/bitbucket_server_status_table_spec.js4
-rw-r--r--spec/frontend/pages/labels/components/promote_label_modal_spec.js4
-rw-r--r--spec/frontend/pages/milestones/shared/components/delete_milestone_modal_spec.js4
-rw-r--r--spec/frontend/pages/milestones/shared/components/promote_milestone_modal_spec.js6
-rw-r--r--spec/frontend/pages/projects/edit/mount_search_settings_spec.js25
-rw-r--r--spec/frontend/pages/projects/forks/new/components/fork_groups_list_item_spec.js7
-rw-r--r--spec/frontend/pages/projects/forks/new/components/fork_groups_list_spec.js11
-rw-r--r--spec/frontend/pages/projects/graphs/code_coverage_spec.js6
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_a_spec.js.snap66
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_b_spec.js.snap66
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_a_spec.js63
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_b_spec.js63
-rw-r--r--spec/frontend/pages/projects/merge_requests/edit/check_form_state_spec.js41
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js2
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/project_feature_settings_spec.js103
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js389
-rw-r--r--spec/frontend/performance_bar/components/detailed_metric_spec.js2
-rw-r--r--spec/frontend/persistent_user_callout_spec.js2
-rw-r--r--spec/frontend/pipeline_editor/components/commit/commit_form_spec.js8
-rw-r--r--spec/frontend/pipeline_editor/components/commit/commit_section_spec.js223
-rw-r--r--spec/frontend/pipeline_editor/components/editor/ci_config_merged_preview_spec.js88
-rw-r--r--spec/frontend/pipeline_editor/components/editor/text_editor_spec.js120
-rw-r--r--spec/frontend/pipeline_editor/components/header/pipeline_editor_header_spec.js34
-rw-r--r--spec/frontend/pipeline_editor/components/header/validation_segment_spec.js (renamed from spec/frontend/pipeline_editor/components/info/validation_segment_spec.js)13
-rw-r--r--spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js4
-rw-r--r--spec/frontend/pipeline_editor/components/lint/ci_lint_spec.js2
-rw-r--r--spec/frontend/pipeline_editor/components/lint/ci_lint_warnings_spec.js2
-rw-r--r--spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js183
-rw-r--r--spec/frontend/pipeline_editor/components/text_editor_spec.js93
-rw-r--r--spec/frontend/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js42
-rw-r--r--spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js4
-rw-r--r--spec/frontend/pipeline_editor/graphql/resolvers_spec.js6
-rw-r--r--spec/frontend/pipeline_editor/mock_data.js3
-rw-r--r--spec/frontend/pipeline_editor/pipeline_editor_app_spec.js425
-rw-r--r--spec/frontend/pipeline_editor/pipeline_editor_home_spec.js78
-rw-r--r--spec/frontend/pipeline_new/components/pipeline_new_form_spec.js129
-rw-r--r--spec/frontend/pipeline_new/utils/format_refs_spec.js2
-rw-r--r--spec/frontend/pipelines/blank_state_spec.js21
-rw-r--r--spec/frontend/pipelines/components/dag/dag_annotations_spec.js2
-rw-r--r--spec/frontend/pipelines/components/dag/dag_graph_spec.js4
-rw-r--r--spec/frontend/pipelines/components/dag/dag_spec.js6
-rw-r--r--spec/frontend/pipelines/components/dag/parsing_utils_spec.js2
-rw-r--r--spec/frontend/pipelines/components/pipelines_filtered_search_spec.js2
-rw-r--r--spec/frontend/pipelines/empty_state_spec.js105
-rw-r--r--spec/frontend/pipelines/graph/action_component_spec.js2
-rw-r--r--spec/frontend/pipelines/graph/graph_component_legacy_spec.js12
-rw-r--r--spec/frontend/pipelines/graph/graph_component_spec.js16
-rw-r--r--spec/frontend/pipelines/graph/graph_component_wrapper_spec.js6
-rw-r--r--spec/frontend/pipelines/graph/job_name_component_spec.js3
-rw-r--r--spec/frontend/pipelines/graph/linked_pipeline_spec.js9
-rw-r--r--spec/frontend/pipelines/graph/linked_pipelines_column_legacy_spec.js4
-rw-r--r--spec/frontend/pipelines/graph/linked_pipelines_column_spec.js113
-rw-r--r--spec/frontend/pipelines/graph/stage_column_component_spec.js4
-rw-r--r--spec/frontend/pipelines/graph_shared/__snapshots__/links_inner_spec.js.snap23
-rw-r--r--spec/frontend/pipelines/graph_shared/links_inner_spec.js197
-rw-r--r--spec/frontend/pipelines/graph_shared/links_layer_spec.js (renamed from spec/frontend/pipelines/shared/links_layer_spec.js)4
-rw-r--r--spec/frontend/pipelines/header_component_spec.js10
-rw-r--r--spec/frontend/pipelines/legacy_header_component_spec.js116
-rw-r--r--spec/frontend/pipelines/pipeline_graph/mock_data.js135
-rw-r--r--spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js8
-rw-r--r--spec/frontend/pipelines/pipeline_triggerer_spec.js2
-rw-r--r--spec/frontend/pipelines/pipeline_url_spec.js28
-rw-r--r--spec/frontend/pipelines/pipelines_actions_spec.js66
-rw-r--r--spec/frontend/pipelines/pipelines_artifacts_spec.js2
-rw-r--r--spec/frontend/pipelines/pipelines_spec.js990
-rw-r--r--spec/frontend/pipelines/pipelines_table_row_spec.js4
-rw-r--r--spec/frontend/pipelines/stage_spec.js278
-rw-r--r--spec/frontend/pipelines/test_reports/stores/actions_spec.js7
-rw-r--r--spec/frontend/pipelines/test_reports/stores/getters_spec.js73
-rw-r--r--spec/frontend/pipelines/test_reports/stores/utils_spec.js16
-rw-r--r--spec/frontend/pipelines/test_reports/test_case_details_spec.js37
-rw-r--r--spec/frontend/pipelines/test_reports/test_reports_spec.js2
-rw-r--r--spec/frontend/pipelines/test_reports/test_suite_table_spec.js44
-rw-r--r--spec/frontend/pipelines/test_reports/test_summary_table_spec.js2
-rw-r--r--spec/frontend/pipelines/time_ago_spec.js2
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js2
-rw-r--r--spec/frontend/popovers/components/popovers_spec.js2
-rw-r--r--spec/frontend/profile/account/components/delete_account_modal_spec.js4
-rw-r--r--spec/frontend/profile/account/components/update_username_spec.js2
-rw-r--r--spec/frontend/profile/preferences/components/__snapshots__/integration_view_spec.js.snap67
-rw-r--r--spec/frontend/profile/preferences/components/__snapshots__/profile_preferences_spec.js.snap51
-rw-r--r--spec/frontend/profile/preferences/components/integration_view_spec.js10
-rw-r--r--spec/frontend/profile/preferences/components/profile_preferences_spec.js142
-rw-r--r--spec/frontend/profile/preferences/mock_data.js2
-rw-r--r--spec/frontend/project_find_file_spec.js2
-rw-r--r--spec/frontend/projects/commit/components/branches_dropdown_spec.js4
-rw-r--r--spec/frontend/projects/commit/components/form_modal_spec.js13
-rw-r--r--spec/frontend/projects/commit/components/form_trigger_spec.js2
-rw-r--r--spec/frontend/projects/commit/store/actions_spec.js6
-rw-r--r--spec/frontend/projects/commit/store/mutations_spec.js2
-rw-r--r--spec/frontend/projects/commit_box/info/load_branches_spec.js2
-rw-r--r--spec/frontend/projects/commits/components/author_select_spec.js2
-rw-r--r--spec/frontend/projects/commits/store/actions_spec.js4
-rw-r--r--spec/frontend/projects/compare/components/app_spec.js116
-rw-r--r--spec/frontend/projects/compare/components/revision_dropdown_spec.js92
-rw-r--r--spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap4
-rw-r--r--spec/frontend/projects/components/shared/delete_button_spec.js2
-rw-r--r--spec/frontend/projects/experiment_new_project_creation/components/app_spec.js4
-rw-r--r--spec/frontend/projects/members/utils_spec.js14
-rw-r--r--spec/frontend/projects/pipelines/charts/components/app_spec.js168
-rw-r--r--spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js94
-rw-r--r--spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js72
-rw-r--r--spec/frontend/projects/pipelines/charts/mock_data.js10
-rw-r--r--spec/frontend/projects/settings/access_dropdown_spec.js1
-rw-r--r--spec/frontend/projects/settings/components/shared_runners_toggle_spec.js2
-rw-r--r--spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js236
-rw-r--r--spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js190
-rw-r--r--spec/frontend/projects/settings_service_desk/services/service_desk_service_spec.js111
-rw-r--r--spec/frontend/prometheus_alerts/components/reset_key_spec.js4
-rw-r--r--spec/frontend/prometheus_metrics/custom_metrics_spec.js2
-rw-r--r--spec/frontend/prometheus_metrics/prometheus_metrics_spec.js2
-rw-r--r--spec/frontend/ref/components/ref_selector_spec.js6
-rw-r--r--spec/frontend/ref/stores/actions_spec.js2
-rw-r--r--spec/frontend/ref/stores/mutations_spec.js6
-rw-r--r--spec/frontend/registry/explorer/components/delete_button_spec.js2
-rw-r--r--spec/frontend/registry/explorer/components/delete_image_spec.js152
-rw-r--r--spec/frontend/registry/explorer/components/details_page/delete_alert_spec.js2
-rw-r--r--spec/frontend/registry/explorer/components/details_page/delete_modal_spec.js68
-rw-r--r--spec/frontend/registry/explorer/components/details_page/details_header_spec.js85
-rw-r--r--spec/frontend/registry/explorer/components/details_page/empty_state_spec.js54
-rw-r--r--spec/frontend/registry/explorer/components/details_page/empty_tags_state_spec.js43
-rw-r--r--spec/frontend/registry/explorer/components/details_page/partial_cleanup_alert_spec.js2
-rw-r--r--spec/frontend/registry/explorer/components/details_page/status_alert_spec.js57
-rw-r--r--spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js10
-rw-r--r--spec/frontend/registry/explorer/components/details_page/tags_list_spec.js32
-rw-r--r--spec/frontend/registry/explorer/components/list_page/cli_commands_spec.js8
-rw-r--r--spec/frontend/registry/explorer/components/list_page/group_empty_state_spec.js6
-rw-r--r--spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js10
-rw-r--r--spec/frontend/registry/explorer/components/list_page/image_list_spec.js2
-rw-r--r--spec/frontend/registry/explorer/components/list_page/project_empty_state_spec.js6
-rw-r--r--spec/frontend/registry/explorer/components/list_page/registry_header_spec.js4
-rw-r--r--spec/frontend/registry/explorer/mock_data.js6
-rw-r--r--spec/frontend/registry/explorer/pages/details_spec.js160
-rw-r--r--spec/frontend/registry/explorer/pages/list_spec.js122
-rw-r--r--spec/frontend/registry/settings/components/expiration_input_spec.js2
-rw-r--r--spec/frontend/registry/settings/components/expiration_run_text_spec.js2
-rw-r--r--spec/frontend/registry/settings/components/expiration_toggle_spec.js2
-rw-r--r--spec/frontend/registry/settings/components/registry_settings_app_spec.js4
-rw-r--r--spec/frontend/registry/settings/components/settings_form_spec.js6
-rw-r--r--spec/frontend/registry/settings/graphql/cache_updated_spec.js2
-rw-r--r--spec/frontend/related_merge_requests/components/related_merge_requests_spec.js2
-rw-r--r--spec/frontend/related_merge_requests/store/actions_spec.js2
-rw-r--r--spec/frontend/related_merge_requests/store/mutations_spec.js2
-rw-r--r--spec/frontend/releases/components/app_edit_new_spec.js8
-rw-r--r--spec/frontend/releases/components/app_index_spec.js10
-rw-r--r--spec/frontend/releases/components/app_show_spec.js6
-rw-r--r--spec/frontend/releases/components/asset_links_form_spec.js4
-rw-r--r--spec/frontend/releases/components/evidence_block_spec.js4
-rw-r--r--spec/frontend/releases/components/release_block_assets_spec.js4
-rw-r--r--spec/frontend/releases/components/release_block_footer_spec.js32
-rw-r--r--spec/frontend/releases/components/release_block_header_spec.js4
-rw-r--r--spec/frontend/releases/components/release_block_milestone_info_spec.js6
-rw-r--r--spec/frontend/releases/components/release_block_spec.js8
-rw-r--r--spec/frontend/releases/components/release_skeleton_loader_spec.js2
-rw-r--r--spec/frontend/releases/components/releases_pagination_graphql_spec.js6
-rw-r--r--spec/frontend/releases/components/releases_pagination_rest_spec.js6
-rw-r--r--spec/frontend/releases/components/releases_sort_spec.js2
-rw-r--r--spec/frontend/releases/components/tag_field_exsting_spec.js2
-rw-r--r--spec/frontend/releases/components/tag_field_new_spec.js4
-rw-r--r--spec/frontend/releases/components/tag_field_spec.js2
-rw-r--r--spec/frontend/releases/stores/modules/detail/actions_spec.js14
-rw-r--r--spec/frontend/releases/stores/modules/detail/mutations_spec.js6
-rw-r--r--spec/frontend/releases/stores/modules/list/actions_spec.js20
-rw-r--r--spec/frontend/releases/stores/modules/list/mutations_spec.js8
-rw-r--r--spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js2
-rw-r--r--spec/frontend/reports/accessibility_report/store/actions_spec.js4
-rw-r--r--spec/frontend/reports/accessibility_report/store/getters_spec.js2
-rw-r--r--spec/frontend/reports/accessibility_report/store/mutations_spec.js2
-rw-r--r--spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js7
-rw-r--r--spec/frontend/reports/codequality_report/mock_data.js50
-rw-r--r--spec/frontend/reports/codequality_report/store/actions_spec.js173
-rw-r--r--spec/frontend/reports/codequality_report/store/getters_spec.js2
-rw-r--r--spec/frontend/reports/codequality_report/store/mutations_spec.js16
-rw-r--r--spec/frontend/reports/codequality_report/store/utils/codequality_comparison_spec.js18
-rw-r--r--spec/frontend/reports/components/grouped_test_reports_app_spec.js9
-rw-r--r--spec/frontend/reports/components/modal_spec.js2
-rw-r--r--spec/frontend/reports/components/report_item_spec.js6
-rw-r--r--spec/frontend/reports/components/report_section_spec.js2
-rw-r--r--spec/frontend/reports/components/summary_row_spec.js4
-rw-r--r--spec/frontend/reports/components/test_issue_body_spec.js2
-rw-r--r--spec/frontend/reports/store/actions_spec.js4
-rw-r--r--spec/frontend/reports/store/mutations_spec.js4
-rw-r--r--spec/frontend/reports/store/utils_spec.js2
-rw-r--r--spec/frontend/repository/components/breadcrumbs_spec.js2
-rw-r--r--spec/frontend/repository/components/last_commit_spec.js98
-rw-r--r--spec/frontend/repository/components/preview/index_spec.js2
-rw-r--r--spec/frontend/repository/components/table/index_spec.js2
-rw-r--r--spec/frontend/repository/components/table/parent_row_spec.js2
-rw-r--r--spec/frontend/repository/components/table/row_spec.js2
-rw-r--r--spec/frontend/repository/components/tree_content_spec.js2
-rw-r--r--spec/frontend/repository/utils/dom_spec.js2
-rw-r--r--spec/frontend/right_sidebar_spec.js2
-rw-r--r--spec/frontend/search/highlight_blob_search_result_spec.js3
-rw-r--r--spec/frontend/search/index_spec.js3
-rw-r--r--spec/frontend/search/mock_data.js22
-rw-r--r--spec/frontend/search/sidebar/components/app_spec.js4
-rw-r--r--spec/frontend/search/sidebar/components/confidentiality_filter_spec.js2
-rw-r--r--spec/frontend/search/sidebar/components/radio_filter_spec.js6
-rw-r--r--spec/frontend/search/sidebar/components/status_filter_spec.js4
-rw-r--r--spec/frontend/search/sort/components/app_spec.js168
-rw-r--r--spec/frontend/search/store/actions_spec.js6
-rw-r--r--spec/frontend/search/store/mutations_spec.js2
-rw-r--r--spec/frontend/search/topbar/components/app_spec.js113
-rw-r--r--spec/frontend/search/topbar/components/group_filter_spec.js2
-rw-r--r--spec/frontend/search/topbar/components/project_filter_spec.js4
-rw-r--r--spec/frontend/search/topbar/components/searchable_dropdown_spec.js4
-rw-r--r--spec/frontend/search_autocomplete_spec.js5
-rw-r--r--spec/frontend/search_settings/components/search_settings_spec.js100
-rw-r--r--spec/frontend/search_settings/index_spec.js37
-rw-r--r--spec/frontend/search_settings/mount_spec.js35
-rw-r--r--spec/frontend/search_spec.js23
-rw-r--r--spec/frontend/security_configuration/app_spec.js27
-rw-r--r--spec/frontend/security_configuration/configuration_table_spec.js48
-rw-r--r--spec/frontend/security_configuration/manage_sast_spec.js136
-rw-r--r--spec/frontend/security_configuration/upgrade_spec.js29
-rw-r--r--spec/frontend/self_monitor/components/self_monitor_form_spec.js2
-rw-r--r--spec/frontend/sentry/index_spec.js2
-rw-r--r--spec/frontend/sentry/sentry_config_spec.js2
-rw-r--r--spec/frontend/sentry_error_stack_trace/components/sentry_error_stack_trace_spec.js2
-rw-r--r--spec/frontend/serverless/components/empty_state_spec.js2
-rw-r--r--spec/frontend/serverless/components/environment_row_spec.js2
-rw-r--r--spec/frontend/serverless/components/function_details_spec.js2
-rw-r--r--spec/frontend/serverless/components/functions_spec.js8
-rw-r--r--spec/frontend/serverless/components/missing_prometheus_spec.js2
-rw-r--r--spec/frontend/serverless/components/url_spec.js2
-rw-r--r--spec/frontend/serverless/store/actions_spec.js2
-rw-r--r--spec/frontend/serverless/store/getters_spec.js2
-rw-r--r--spec/frontend/serverless/store/mutations_spec.js2
-rw-r--r--spec/frontend/serverless/survey_banner_spec.js2
-rw-r--r--spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js2
-rw-r--r--spec/frontend/set_status_modal/user_availability_status_spec.js31
-rw-r--r--spec/frontend/set_status_modal/utils_spec.js15
-rw-r--r--spec/frontend/settings_panels_spec.js10
-rw-r--r--spec/frontend/sidebar/__snapshots__/todo_spec.js.snap2
-rw-r--r--spec/frontend/sidebar/assignee_title_spec.js2
-rw-r--r--spec/frontend/sidebar/assignees_realtime_spec.js4
-rw-r--r--spec/frontend/sidebar/assignees_spec.js2
-rw-r--r--spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js32
-rw-r--r--spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js26
-rw-r--r--spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js6
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js120
-rw-r--r--spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js2
-rw-r--r--spec/frontend/sidebar/components/assignees/user_name_with_status_spec.js51
-rw-r--r--spec/frontend/sidebar/components/copy_email_to_clipboard_spec.js4
-rw-r--r--spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js91
-rw-r--r--spec/frontend/sidebar/components/severity/severity_spec.js4
-rw-r--r--spec/frontend/sidebar/components/severity/sidebar_severity_spec.js8
-rw-r--r--spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js2
-rw-r--r--spec/frontend/sidebar/confidential/edit_form_buttons_spec.js4
-rw-r--r--spec/frontend/sidebar/confidential_issue_sidebar_spec.js6
-rw-r--r--spec/frontend/sidebar/lock/edit_form_buttons_spec.js6
-rw-r--r--spec/frontend/sidebar/lock/issuable_lock_form_spec.js6
-rw-r--r--spec/frontend/sidebar/reviewer_title_spec.js2
-rw-r--r--spec/frontend/sidebar/reviewers_spec.js9
-rw-r--r--spec/frontend/sidebar/sidebar_assignees_spec.js6
-rw-r--r--spec/frontend/sidebar/sidebar_move_issue_spec.js6
-rw-r--r--spec/frontend/sidebar/sidebar_subscriptions_spec.js2
-rw-r--r--spec/frontend/sidebar/subscriptions_spec.js25
-rw-r--r--spec/frontend/sidebar/todo_spec.js6
-rw-r--r--spec/frontend/sidebar/user_data_mock.js2
-rw-r--r--spec/frontend/snippets/components/edit_spec.js585
-rw-r--r--spec/frontend/snippets/components/embed_dropdown_spec.js4
-rw-r--r--spec/frontend/snippets/components/show_spec.js9
-rw-r--r--spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js2
-rw-r--r--spec/frontend/snippets/components/snippet_blob_edit_spec.js8
-rw-r--r--spec/frontend/snippets/components/snippet_blob_view_spec.js8
-rw-r--r--spec/frontend/snippets/components/snippet_header_spec.js8
-rw-r--r--spec/frontend/snippets/components/snippet_title_spec.js2
-rw-r--r--spec/frontend/snippets/test_utils.js55
-rw-r--r--spec/frontend/snippets/utils/error_spec.js16
-rw-r--r--spec/frontend/static_site_editor/components/edit_area_spec.js11
-rw-r--r--spec/frontend/static_site_editor/components/edit_drawer_spec.js3
-rw-r--r--spec/frontend/static_site_editor/components/edit_meta_controls_spec.js3
-rw-r--r--spec/frontend/static_site_editor/components/edit_meta_modal_spec.js8
-rw-r--r--spec/frontend/static_site_editor/components/front_matter_controls_spec.js2
-rw-r--r--spec/frontend/static_site_editor/components/submit_changes_error_spec.js2
-rw-r--r--spec/frontend/static_site_editor/graphql/resolvers/submit_content_changes_spec.js2
-rw-r--r--spec/frontend/static_site_editor/pages/home_spec.js8
-rw-r--r--spec/frontend/static_site_editor/pages/success_spec.js4
-rw-r--r--spec/frontend/static_site_editor/services/front_matterify_spec.js3
-rw-r--r--spec/frontend/static_site_editor/services/parse_source_file_spec.js3
-rw-r--r--spec/frontend/task_list_spec.js2
-rw-r--r--spec/frontend/terraform/components/states_table_actions_spec.js181
-rw-r--r--spec/frontend/terraform/components/states_table_spec.js56
-rw-r--r--spec/frontend/terraform/components/terraform_list_spec.js41
-rw-r--r--spec/frontend/test_setup.js14
-rw-r--r--spec/frontend/tooltips/components/tooltips_spec.js2
-rw-r--r--spec/frontend/tooltips/index_spec.js26
-rw-r--r--spec/frontend/user_lists/components/edit_user_list_spec.js8
-rw-r--r--spec/frontend/user_lists/components/new_user_list_spec.js6
-rw-r--r--spec/frontend/user_lists/components/user_list_spec.js10
-rw-r--r--spec/frontend/user_lists/store/edit/actions_spec.js6
-rw-r--r--spec/frontend/user_lists/store/edit/mutations_spec.js2
-rw-r--r--spec/frontend/user_lists/store/new/actions_spec.js6
-rw-r--r--spec/frontend/user_lists/store/new/mutations_spec.js2
-rw-r--r--spec/frontend/user_lists/store/show/actions_spec.js6
-rw-r--r--spec/frontend/user_lists/store/show/mutations_spec.js4
-rw-r--r--spec/frontend/user_popovers_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js4
-rw-r--r--spec/frontend/vue_mr_widget/components/approvals/approvals_summary_optional_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/approvals/approvals_summary_spec.js4
-rw-r--r--spec/frontend/vue_mr_widget/components/artifacts_list_app_spec.js6
-rw-r--r--spec/frontend/vue_mr_widget/components/artifacts_list_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_alert_message_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_icon_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_memory_usage_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_pipeline_container_spec.js6
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_pipeline_spec.js6
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_rebase_spec.js4
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_status_icon_spec.js46
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_suggest_pipeline_spec.js12
-rw-r--r--spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_pipeline_failed_spec.js.snap24
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js12
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js4
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js4
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js10
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js24
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js26
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js39
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js14
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js4
-rw-r--r--spec/frontend/vue_mr_widget/components/terraform/terraform_plan_spec.js8
-rw-r--r--spec/frontend/vue_mr_widget/deployment/deployment_action_button_spec.js4
-rw-r--r--spec/frontend/vue_mr_widget/deployment/deployment_actions_spec.js4
-rw-r--r--spec/frontend/vue_mr_widget/deployment/deployment_spec.js6
-rw-r--r--spec/frontend/vue_mr_widget/mr_widget_options_spec.js83
-rw-r--r--spec/frontend/vue_mr_widget/stores/artifacts_list/actions_spec.js4
-rw-r--r--spec/frontend/vue_mr_widget/stores/artifacts_list/mutations_spec.js4
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_details_spec.js (renamed from spec/frontend/alert_management/components/alert_details_spec.js)59
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js (renamed from spec/frontend/alert_management/components/alert_management_sidebar_todo_spec.js)8
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_metrics_spec.js (renamed from spec/frontend/alert_management/components/alert_metrics_spec.js)6
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_status_spec.js (renamed from spec/frontend/alert_management/components/alert_status_spec.js)29
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_summary_row_spec.js (renamed from spec/frontend/alert_management/components/alert_summary_row_spec.js)2
-rw-r--r--spec/frontend/vue_shared/alert_details/mocks/alerts.json (renamed from spec/frontend/alert_management/mocks/alerts.json)0
-rw-r--r--spec/frontend/vue_shared/alert_details/sidebar/alert_managment_sidebar_assignees_spec.js (renamed from spec/frontend/alert_management/components/sidebar/alert_managment_sidebar_assignees_spec.js)10
-rw-r--r--spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_spec.js (renamed from spec/frontend/alert_management/components/sidebar/alert_sidebar_spec.js)39
-rw-r--r--spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js (renamed from spec/frontend/alert_management/components/sidebar/alert_sidebar_status_spec.js)35
-rw-r--r--spec/frontend/vue_shared/alert_details/system_notes/alert_management_system_note_spec.js (renamed from spec/frontend/alert_management/components/system_notes/alert_management_system_note_spec.js)6
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap28
-rw-r--r--spec/frontend/vue_shared/components/actions_button_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/changed_file_icon_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/ci_badge_link_spec.js40
-rw-r--r--spec/frontend/vue_shared/components/ci_icon_spec.js151
-rw-r--r--spec/frontend/vue_shared/components/clipboard_button_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/clone_dropdown_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/color_picker/color_picker_spec.js39
-rw-r--r--spec/frontend/vue_shared/components/commit_spec.js28
-rw-r--r--spec/frontend/vue_shared/components/content_viewer/viewers/image_viewer_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/dismissible_alert_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/dismissible_container_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/editor_lite_spec.js11
-rw-r--r--spec/frontend/vue_shared/components/expand_button_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/file_finder/index_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/file_finder/item_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/file_icon_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/file_row_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/file_tree_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js9
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/mutations_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/gfm_autocomplete/__snapshots__/utils_spec.js.snap16
-rw-r--r--spec/frontend/vue_shared/components/gl_countdown_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/gl_modal_vuex_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/gl_toggle_vuex_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/header_ci_component_spec.js118
-rw-r--r--spec/frontend/vue_shared/components/help_popover_spec.js65
-rw-r--r--spec/frontend/vue_shared/components/integration_help_text_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/issuable/issuable_header_warnings_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_assignees_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_milestone_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/markdown/apply_suggestion_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/markdown/field_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/markdown/field_view_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/memory_graph_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/modal_copy_button_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/multiselect_dropdown_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/navigation_tabs_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/notes/noteable_warning_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/notes/system_note_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/pagination_links_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/pikaday_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/project_selector/project_selector_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/registry/code_instruction_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/registry/details_row_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/registry/history_item_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/registry/metadata_item_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/registry/registry_search_spec.js105
-rw-r--r--spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/editor_service_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/modals/insert_video_modal_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_integration_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_font_awesome_html_inline_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_html_block_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js9
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/toolbar_item_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/mock_data.js107
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js113
-rw-r--r--spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap43
-rw-r--r--spec/frontend/vue_shared/components/settings/settings_block_spec.js86
-rw-r--r--spec/frontend/vue_shared/components/sidebar/date_picker_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js42
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js9
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js42
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/smart_virtual_list_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/table_pagination_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/tabs/tabs_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/todo_button_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/toggle_button_spec.js96
-rw-r--r--spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/user_popover/user_popover_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/web_ide_link_spec.js2
-rw-r--r--spec/frontend/vue_shared/directives/tooltip_spec.js2
-rw-r--r--spec/frontend/vue_shared/directives/track_event_spec.js4
-rw-r--r--spec/frontend/vue_shared/security_reports/mock_data.js17
-rw-r--r--spec/frontend/vue_shared/security_reports/security_reports_app_spec.js374
-rw-r--r--spec/frontend/vue_shared/security_reports/store/getters_spec.js8
-rw-r--r--spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js6
-rw-r--r--spec/frontend/vue_shared/security_reports/store/modules/sast/mutations_spec.js2
-rw-r--r--spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js6
-rw-r--r--spec/frontend/vue_shared/security_reports/store/modules/secret_detection/mutations_spec.js2
-rw-r--r--spec/frontend/vue_shared/security_reports/utils_spec.js2
-rw-r--r--spec/frontend/vuex_shared/modules/modal/actions_spec.js2
-rw-r--r--spec/frontend/vuex_shared/modules/modal/mutations_spec.js2
-rw-r--r--spec/frontend/whats_new/components/app_spec.js2
-rw-r--r--spec/frontend/whats_new/store/actions_spec.js6
-rw-r--r--spec/frontend/whats_new/store/mutations_spec.js2
-rw-r--r--spec/frontend/whats_new/utils/get_drawer_body_height_spec.js2
-rw-r--r--spec/frontend/zen_mode_spec.js4
-rw-r--r--spec/frontend_integration/ide/helpers/ide_helper.js8
-rw-r--r--spec/frontend_integration/ide/helpers/start.js13
-rw-r--r--spec/frontend_integration/ide/ide_integration_spec.js6
-rw-r--r--spec/frontend_integration/ide/user_opens_file_spec.js2
-rw-r--r--spec/frontend_integration/ide/user_opens_ide_spec.js2
-rw-r--r--spec/frontend_integration/ide/user_opens_mr_spec.js60
-rw-r--r--spec/frontend_integration/test_helpers/factories/commit.js2
-rw-r--r--spec/frontend_integration/test_helpers/fixtures.js14
-rw-r--r--spec/frontend_integration/test_helpers/mock_server/index.js6
-rw-r--r--spec/frontend_integration/test_helpers/mock_server/routes/404.js2
-rw-r--r--spec/frontend_integration/test_helpers/mock_server/routes/projects.js18
-rw-r--r--spec/graphql/mutations/boards/lists/create_spec.rb78
-rw-r--r--spec/graphql/mutations/concerns/mutations/can_mutate_spammable_spec.rb46
-rw-r--r--spec/graphql/mutations/merge_requests/update_spec.rb21
-rw-r--r--spec/graphql/mutations/security/ci_configuration/configure_sast_spec.rb120
-rw-r--r--spec/graphql/resolvers/base_resolver_spec.rb4
-rw-r--r--spec/graphql/resolvers/board_list_issues_resolver_spec.rb6
-rw-r--r--spec/graphql/resolvers/board_lists_resolver_spec.rb12
-rw-r--r--spec/graphql/resolvers/ci/config_resolver_spec.rb3
-rw-r--r--spec/graphql/resolvers/container_repositories_resolver_spec.rb28
-rw-r--r--spec/graphql/resolvers/group_labels_resolver_spec.rb96
-rw-r--r--spec/graphql/resolvers/issues_resolver_spec.rb12
-rw-r--r--spec/graphql/resolvers/labels_resolver_spec.rb96
-rw-r--r--spec/graphql/resolvers/merge_requests_resolver_spec.rb62
-rw-r--r--spec/graphql/resolvers/package_details_resolver_spec.rb5
-rw-r--r--spec/graphql/resolvers/packages_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/release_milestones_resolver_spec.rb9
-rw-r--r--spec/graphql/resolvers/release_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/terraform/states_resolver_spec.rb20
-rw-r--r--spec/graphql/types/ci/pipeline_type_spec.rb2
-rw-r--r--spec/graphql/types/ci_configuration/sast/analyzers_entity_input_type_spec.rb9
-rw-r--r--spec/graphql/types/ci_configuration/sast/analyzers_entity_type_spec.rb11
-rw-r--r--spec/graphql/types/ci_configuration/sast/entity_input_type_spec.rb9
-rw-r--r--spec/graphql/types/ci_configuration/sast/entity_type_spec.rb11
-rw-r--r--spec/graphql/types/ci_configuration/sast/input_type_spec.rb9
-rw-r--r--spec/graphql/types/ci_configuration/sast/options_entity_spec.rb11
-rw-r--r--spec/graphql/types/ci_configuration/sast/type_spec.rb11
-rw-r--r--spec/graphql/types/ci_configuration/sast/ui_component_size_enum_spec.rb11
-rw-r--r--spec/graphql/types/container_repository_sort_enum_spec.rb15
-rw-r--r--spec/graphql/types/event_type_spec.rb11
-rw-r--r--spec/graphql/types/eventable_type_spec.rb9
-rw-r--r--spec/graphql/types/group_type_spec.rb4
-rw-r--r--spec/graphql/types/merge_request_state_event_enum_spec.rb12
-rw-r--r--spec/graphql/types/packages/composer/details_type_spec.rb23
-rw-r--r--spec/graphql/types/packages/composer/metadatum_type_spec.rb4
-rw-r--r--spec/graphql/types/packages/package_type_enum_spec.rb2
-rw-r--r--spec/graphql/types/packages/package_type_spec.rb7
-rw-r--r--spec/graphql/types/packages/package_without_versions_type_spec.rb13
-rw-r--r--spec/graphql/types/project_type_spec.rb172
-rw-r--r--spec/graphql/types/query_type_spec.rb6
-rw-r--r--spec/graphql/types/user_type_spec.rb1
-rw-r--r--spec/helpers/analytics/unique_visits_helper_spec.rb13
-rw-r--r--spec/helpers/application_settings_helper_spec.rb29
-rw-r--r--spec/helpers/auth_helper_spec.rb4
-rw-r--r--spec/helpers/commits_helper_spec.rb104
-rw-r--r--spec/helpers/container_registry_helper_spec.rb4
-rw-r--r--spec/helpers/diff_helper_spec.rb48
-rw-r--r--spec/helpers/enable_search_settings_helper_spec.rb21
-rw-r--r--spec/helpers/events_helper_spec.rb8
-rw-r--r--spec/helpers/groups/group_members_helper_spec.rb16
-rw-r--r--spec/helpers/groups_helper_spec.rb78
-rw-r--r--spec/helpers/invite_members_helper_spec.rb87
-rw-r--r--spec/helpers/issuables_description_templates_helper_spec.rb93
-rw-r--r--spec/helpers/issuables_helper_spec.rb47
-rw-r--r--spec/helpers/issues_helper_spec.rb27
-rw-r--r--spec/helpers/jira_connect_helper_spec.rb41
-rw-r--r--spec/helpers/learn_gitlab_helper_spec.rb91
-rw-r--r--spec/helpers/merge_requests_helper_spec.rb10
-rw-r--r--spec/helpers/notes_helper_spec.rb11
-rw-r--r--spec/helpers/notify_helper_spec.rb31
-rw-r--r--spec/helpers/operations_helper_spec.rb2
-rw-r--r--spec/helpers/projects/alert_management_helper_spec.rb7
-rw-r--r--spec/helpers/projects/project_members_helper_spec.rb56
-rw-r--r--spec/helpers/projects_helper_spec.rb113
-rw-r--r--spec/helpers/search_helper_spec.rb31
-rw-r--r--spec/helpers/sorting_helper_spec.rb18
-rw-r--r--spec/helpers/stat_anchors_helper_spec.rb4
-rw-r--r--spec/helpers/tree_helper_spec.rb88
-rw-r--r--spec/helpers/user_callouts_helper_spec.rb20
-rw-r--r--spec/initializers/lograge_spec.rb24
-rw-r--r--spec/initializers/net_http_patch_spec.rb86
-rw-r--r--spec/initializers/validate_puma_spec.rb67
-rw-r--r--spec/javascripts/fly_out_nav_browser_spec.js2
-rw-r--r--spec/javascripts/matchers.js42
-rw-r--r--spec/javascripts/monitoring/components/dashboard_resize_browser_spec.js6
-rw-r--r--spec/javascripts/test_bundle.js17
-rw-r--r--spec/lib/api/entities/merge_request_basic_spec.rb27
-rw-r--r--spec/lib/api/entities/user_spec.rb12
-rw-r--r--spec/lib/api/support/git_access_actor_spec.rb12
-rw-r--r--spec/lib/atlassian/jira_connect/client_spec.rb66
-rw-r--r--spec/lib/atlassian/jira_connect/serializers/feature_flag_entity_spec.rb5
-rw-r--r--spec/lib/backup/files_spec.rb14
-rw-r--r--spec/lib/banzai/filter/asset_proxy_filter_spec.rb16
-rw-r--r--spec/lib/banzai/filter/custom_emoji_filter_spec.rb80
-rw-r--r--spec/lib/banzai/filter/feature_flag_reference_filter_spec.rb223
-rw-r--r--spec/lib/banzai/filter/merge_request_reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/truncate_source_filter_spec.rb2
-rw-r--r--spec/lib/banzai/pipeline/broadcast_message_pipeline_spec.rb5
-rw-r--r--spec/lib/banzai/pipeline/full_pipeline_spec.rb12
-rw-r--r--spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb118
-rw-r--r--spec/lib/banzai/pipeline/pre_process_pipeline_spec.rb2
-rw-r--r--spec/lib/banzai/reference_parser/feature_flag_parser_spec.rb53
-rw-r--r--spec/lib/bulk_imports/common/extractors/graphql_extractor_spec.rb66
-rw-r--r--spec/lib/bulk_imports/common/loaders/entity_loader_spec.rb2
-rw-r--r--spec/lib/bulk_imports/common/transformers/award_emoji_transformer_spec.rb48
-rw-r--r--spec/lib/bulk_imports/common/transformers/hash_key_digger_spec.rb28
-rw-r--r--spec/lib/bulk_imports/common/transformers/underscorify_keys_transformer_spec.rb27
-rw-r--r--spec/lib/bulk_imports/groups/extractors/subgroups_extractor_spec.rb24
-rw-r--r--spec/lib/bulk_imports/groups/graphql/get_group_query_spec.rb32
-rw-r--r--spec/lib/bulk_imports/groups/graphql/get_labels_query_spec.rb32
-rw-r--r--spec/lib/bulk_imports/groups/graphql/get_members_query_spec.rb35
-rw-r--r--spec/lib/bulk_imports/groups/loaders/group_loader_spec.rb17
-rw-r--r--spec/lib/bulk_imports/groups/loaders/labels_loader_spec.rb30
-rw-r--r--spec/lib/bulk_imports/groups/loaders/members_loader_spec.rb42
-rw-r--r--spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb51
-rw-r--r--spec/lib/bulk_imports/groups/pipelines/labels_pipeline_spec.rb118
-rw-r--r--spec/lib/bulk_imports/groups/pipelines/members_pipeline_spec.rb87
-rw-r--r--spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb12
-rw-r--r--spec/lib/bulk_imports/groups/transformers/group_attributes_transformer_spec.rb24
-rw-r--r--spec/lib/bulk_imports/groups/transformers/member_attributes_transformer_spec.rb101
-rw-r--r--spec/lib/bulk_imports/importers/group_importer_spec.rb29
-rw-r--r--spec/lib/bulk_imports/pipeline/context_spec.rb38
-rw-r--r--spec/lib/bulk_imports/pipeline/extracted_data_spec.rb53
-rw-r--r--spec/lib/bulk_imports/pipeline/runner_spec.rb97
-rw-r--r--spec/lib/feature/gitaly_spec.rb67
-rw-r--r--spec/lib/gitlab/access/branch_protection_spec.rb10
-rw-r--r--spec/lib/gitlab/alert_management/payload/generic_spec.rb29
-rw-r--r--spec/lib/gitlab/alert_management/payload/prometheus_spec.rb63
-rw-r--r--spec/lib/gitlab/alert_management/payload_spec.rb15
-rw-r--r--spec/lib/gitlab/api_authentication/token_locator_spec.rb21
-rw-r--r--spec/lib/gitlab/api_authentication/token_resolver_spec.rb63
-rw-r--r--spec/lib/gitlab/asciidoc_spec.rb67
-rw-r--r--spec/lib/gitlab/auth/auth_finders_spec.rb329
-rw-r--r--spec/lib/gitlab/auth/ip_rate_limiter_spec.rb3
-rw-r--r--spec/lib/gitlab/auth/otp/session_enforcer_spec.rb41
-rw-r--r--spec/lib/gitlab/auth/u2f_webauthn_converter_spec.rb29
-rw-r--r--spec/lib/gitlab/background_migration/backfill_project_updated_at_after_repository_storage_move_spec.rb35
-rw-r--r--spec/lib/gitlab/background_migration/populate_issue_email_participants_spec.rb20
-rw-r--r--spec/lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings_spec.rb135
-rw-r--r--spec/lib/gitlab/background_migration_spec.rb2
-rw-r--r--spec/lib/gitlab/changelog/ast_spec.rb246
-rw-r--r--spec/lib/gitlab/changelog/committer_spec.rb128
-rw-r--r--spec/lib/gitlab/changelog/config_spec.rb98
-rw-r--r--spec/lib/gitlab/changelog/generator_spec.rb164
-rw-r--r--spec/lib/gitlab/changelog/parser_spec.rb78
-rw-r--r--spec/lib/gitlab/changelog/release_spec.rb129
-rw-r--r--spec/lib/gitlab/ci/badge/coverage/metadata_spec.rb (renamed from spec/lib/gitlab/badge/coverage/metadata_spec.rb)4
-rw-r--r--spec/lib/gitlab/ci/badge/coverage/report_spec.rb (renamed from spec/lib/gitlab/badge/coverage/report_spec.rb)2
-rw-r--r--spec/lib/gitlab/ci/badge/coverage/template_spec.rb (renamed from spec/lib/gitlab/badge/coverage/template_spec.rb)2
-rw-r--r--spec/lib/gitlab/ci/badge/pipeline/metadata_spec.rb (renamed from spec/lib/gitlab/badge/pipeline/metadata_spec.rb)4
-rw-r--r--spec/lib/gitlab/ci/badge/pipeline/status_spec.rb (renamed from spec/lib/gitlab/badge/pipeline/status_spec.rb)2
-rw-r--r--spec/lib/gitlab/ci/badge/pipeline/template_spec.rb (renamed from spec/lib/gitlab/badge/pipeline/template_spec.rb)2
-rw-r--r--spec/lib/gitlab/ci/badge/shared/metadata.rb (renamed from spec/lib/gitlab/badge/shared/metadata.rb)0
-rw-r--r--spec/lib/gitlab/ci/build/credentials/registry/dependency_proxy_spec.rb43
-rw-r--r--spec/lib/gitlab/ci/build/credentials/registry/gitlab_registry_spec.rb (renamed from spec/lib/gitlab/ci/build/credentials/registry_spec.rb)2
-rw-r--r--spec/lib/gitlab/ci/build/rules_spec.rb29
-rw-r--r--spec/lib/gitlab/ci/charts_spec.rb82
-rw-r--r--spec/lib/gitlab/ci/config/entry/cache_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/config/entry/commands_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/config/entry/job_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/config/entry/processable_spec.rb29
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper_spec.rb14
-rw-r--r--spec/lib/gitlab/ci/config/yaml/tags/reference_spec.rb121
-rw-r--r--spec/lib/gitlab/ci/config/yaml/tags/resolver_spec.rb123
-rw-r--r--spec/lib/gitlab/ci/config_spec.rb20
-rw-r--r--spec/lib/gitlab/ci/cron_parser_spec.rb11
-rw-r--r--spec/lib/gitlab/ci/jwt_spec.rb36
-rw-r--r--spec/lib/gitlab/ci/parsers/instrumentation_spec.rb27
-rw-r--r--spec/lib/gitlab/ci/parsers_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/build_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb14
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/template_usage_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/build_spec.rb13
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/processable/resource_group_spec.rb (renamed from spec/lib/gitlab/ci/pipeline/seed/build/resource_group_spec.rb)2
-rw-r--r--spec/lib/gitlab/ci/reports/codequality_mr_diff_spec.rb58
-rw-r--r--spec/lib/gitlab/ci/reports/codequality_reports_comparer_spec.rb58
-rw-r--r--spec/lib/gitlab/ci/reports/codequality_reports_spec.rb58
-rw-r--r--spec/lib/gitlab/ci/status/bridge/factory_spec.rb21
-rw-r--r--spec/lib/gitlab/ci/status/bridge/waiting_for_resource_spec.rb7
-rw-r--r--spec/lib/gitlab/ci/status/build/waiting_for_resource_spec.rb7
-rw-r--r--spec/lib/gitlab/ci/status/processable/waiting_for_resource_spec.rb36
-rw-r--r--spec/lib/gitlab/ci/trace/chunked_io_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/variables/collection/sorted_spec.rb20
-rw-r--r--spec/lib/gitlab/ci/variables/helpers_spec.rb103
-rw-r--r--spec/lib/gitlab/cleanup/orphan_job_artifact_files_spec.rb3
-rw-r--r--spec/lib/gitlab/cluster/lifecycle_events_spec.rb85
-rw-r--r--spec/lib/gitlab/composer/cache_spec.rb133
-rw-r--r--spec/lib/gitlab/composer/version_index_spec.rb16
-rw-r--r--spec/lib/gitlab/conan_token_spec.rb2
-rw-r--r--spec/lib/gitlab/config/entry/validators/nested_array_helpers_spec.rb70
-rw-r--r--spec/lib/gitlab/crypto_helper_spec.rb78
-rw-r--r--spec/lib/gitlab/current_settings_spec.rb28
-rw-r--r--spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb34
-rw-r--r--spec/lib/gitlab/data_builder/build_spec.rb4
-rw-r--r--spec/lib/gitlab/data_builder/pipeline_spec.rb15
-rw-r--r--spec/lib/gitlab/database/migration_helpers/v2_spec.rb221
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb253
-rw-r--r--spec/lib/gitlab/database/migrations/instrumentation_spec.rb111
-rw-r--r--spec/lib/gitlab/database/migrations/observers/total_database_size_change_spec.rb39
-rw-r--r--spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb3
-rw-r--r--spec/lib/gitlab/database/with_lock_retries_spec.rb4
-rw-r--r--spec/lib/gitlab/diff/char_diff_spec.rb77
-rw-r--r--spec/lib/gitlab/diff/file_collection_sorter_spec.rb10
-rw-r--r--spec/lib/gitlab/diff/highlight_cache_spec.rb18
-rw-r--r--spec/lib/gitlab/diff/inline_diff_spec.rb27
-rw-r--r--spec/lib/gitlab/email/handler/service_desk_handler_spec.rb7
-rw-r--r--spec/lib/gitlab/experimentation/controller_concern_spec.rb27
-rw-r--r--spec/lib/gitlab/experimentation/experiment_spec.rb3
-rw-r--r--spec/lib/gitlab/experimentation_spec.rb66
-rw-r--r--spec/lib/gitlab/file_finder_spec.rb8
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb3
-rw-r--r--spec/lib/gitlab/git/diff_spec.rb7
-rw-r--r--spec/lib/gitlab/git/push_spec.rb10
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb5
-rw-r--r--spec/lib/gitlab/git_access_spec.rb102
-rw-r--r--spec/lib/gitlab/gitaly_client/operation_service_spec.rb10
-rw-r--r--spec/lib/gitlab/gitaly_client_spec.rb64
-rw-r--r--spec/lib/gitlab/graphql/pagination/connections_spec.rb97
-rw-r--r--spec/lib/gitlab/graphql/queries_spec.rb4
-rw-r--r--spec/lib/gitlab/health_checks/master_check_spec.rb68
-rw-r--r--spec/lib/gitlab/health_checks/probes/collection_spec.rb29
-rw-r--r--spec/lib/gitlab/hook_data/group_builder_spec.rb68
-rw-r--r--spec/lib/gitlab/hook_data/subgroup_builder_spec.rb52
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml7
-rw-r--r--spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb50
-rw-r--r--spec/lib/gitlab/import_export/design_repo_restorer_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/design_repo_saver_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/fork_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/group/tree_restorer_spec.rb1
-rw-r--r--spec/lib/gitlab/import_export/importer_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/repo_restorer_spec.rb10
-rw-r--r--spec/lib/gitlab/import_export/repo_saver_spec.rb10
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml4
-rw-r--r--spec/lib/gitlab/import_export/saver_spec.rb12
-rw-r--r--spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb2
-rw-r--r--spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb1
-rw-r--r--spec/lib/gitlab/instrumentation_helper_spec.rb60
-rw-r--r--spec/lib/gitlab/kas_spec.rb44
-rw-r--r--spec/lib/gitlab/memory/instrumentation_spec.rb100
-rw-r--r--spec/lib/gitlab/metrics/subscribers/external_http_spec.rb172
-rw-r--r--spec/lib/gitlab/metrics/subscribers/rack_attack_spec.rb203
-rw-r--r--spec/lib/gitlab/middleware/request_context_spec.rb43
-rw-r--r--spec/lib/gitlab/pages_transfer_spec.rb22
-rw-r--r--spec/lib/gitlab/patch/prependable_spec.rb18
-rw-r--r--spec/lib/gitlab/performance_bar/stats_spec.rb8
-rw-r--r--spec/lib/gitlab/rack_attack/instrumented_cache_store_spec.rb89
-rw-r--r--spec/lib/gitlab/rack_attack_spec.rb3
-rw-r--r--spec/lib/gitlab/repository_cache_adapter_spec.rb7
-rw-r--r--spec/lib/gitlab/request_forgery_protection_spec.rb5
-rw-r--r--spec/lib/gitlab/runtime_spec.rb17
-rw-r--r--spec/lib/gitlab/search/query_spec.rb18
-rw-r--r--spec/lib/gitlab/search_results_spec.rb27
-rw-r--r--spec/lib/gitlab/sidekiq_logging/exception_handler_spec.rb44
-rw-r--r--spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb178
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb21
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb22
-rw-r--r--spec/lib/gitlab/suggestions/commit_message_spec.rb11
-rw-r--r--spec/lib/gitlab/template/finders/global_template_finder_spec.rb23
-rw-r--r--spec/lib/gitlab/terraform/state_migration_helper_spec.rb21
-rw-r--r--spec/lib/gitlab/tracking/standard_context_spec.rb55
-rw-r--r--spec/lib/gitlab/tracking_spec.rb47
-rw-r--r--spec/lib/gitlab/url_blocker_spec.rb46
-rw-r--r--spec/lib/gitlab/url_blockers/url_allowlist_spec.rb28
-rw-r--r--spec/lib/gitlab/usage/docs/renderer_spec.rb22
-rw-r--r--spec/lib/gitlab/usage/docs/value_formatter_spec.rb26
-rw-r--r--spec/lib/gitlab/usage/metric_definition_spec.rb29
-rw-r--r--spec/lib/gitlab/usage/metric_spec.rb8
-rw-r--r--spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb281
-rw-r--r--spec/lib/gitlab/usage/metrics/aggregates/sources/postgres_hll_spec.rb146
-rw-r--r--spec/lib/gitlab/usage/metrics/aggregates/sources/redis_hll_spec.rb29
-rw-r--r--spec/lib/gitlab/usage_data_counters/aggregated_metrics_spec.rb20
-rw-r--r--spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb86
-rw-r--r--spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb228
-rw-r--r--spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb136
-rw-r--r--spec/lib/gitlab/usage_data_counters/quick_action_activity_unique_counter_spec.rb163
-rw-r--r--spec/lib/gitlab/usage_data_counters/vs_code_extenion_activity_unique_counter_spec.rb63
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb144
-rw-r--r--spec/lib/gitlab/utils/markdown_spec.rb44
-rw-r--r--spec/lib/gitlab/utils/override_spec.rb67
-rw-r--r--spec/lib/gitlab/utils/usage_data_spec.rb10
-rw-r--r--spec/lib/gitlab/utils_spec.rb8
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb47
-rw-r--r--spec/lib/gitlab_spec.rb24
-rw-r--r--spec/lib/object_storage/config_spec.rb63
-rw-r--r--spec/lib/peek/views/external_http_spec.rb215
-rw-r--r--spec/lib/release_highlights/validator/entry_spec.rb2
-rw-r--r--spec/lib/release_highlights/validator_spec.rb2
-rw-r--r--spec/lib/security/ci_configuration/sast_build_actions_spec.rb539
-rw-r--r--spec/mailers/notify_spec.rb22
-rw-r--r--spec/migrations/20201112130710_schedule_remove_duplicate_vulnerabilities_findings_spec.rb140
-rw-r--r--spec/migrations/20210119122354_alter_vsa_issue_first_mentioned_in_commit_value_spec.rb30
-rw-r--r--spec/migrations/20210205174154_remove_bad_dependency_proxy_manifests_spec.rb28
-rw-r--r--spec/migrations/20210210093901_backfill_updated_at_after_repository_storage_move_spec.rb47
-rw-r--r--spec/migrations/add_has_external_issue_tracker_trigger_spec.rb164
-rw-r--r--spec/migrations/add_new_post_eoa_plans_spec.rb32
-rw-r--r--spec/migrations/cleanup_projects_with_bad_has_external_wiki_data_spec.rb89
-rw-r--r--spec/migrations/drop_alerts_service_data_spec.rb21
-rw-r--r--spec/migrations/encrypt_feature_flags_clients_tokens_spec.rb2
-rw-r--r--spec/migrations/remove_alerts_service_records_again_spec.rb23
-rw-r--r--spec/migrations/schedule_migrate_security_scans_spec.rb11
-rw-r--r--spec/migrations/schedule_populate_issue_email_participants_spec.rb33
-rw-r--r--spec/models/active_session_spec.rb2
-rw-r--r--spec/models/application_setting_spec.rb77
-rw-r--r--spec/models/bulk_imports/entity_spec.rb31
-rw-r--r--spec/models/ci/bridge_spec.rb8
-rw-r--r--spec/models/ci/build_dependencies_spec.rb28
-rw-r--r--spec/models/ci/build_spec.rb80
-rw-r--r--spec/models/ci/build_trace_chunk_spec.rb2
-rw-r--r--spec/models/ci/build_trace_chunks/fog_spec.rb21
-rw-r--r--spec/models/ci/daily_build_group_report_result_spec.rb43
-rw-r--r--spec/models/ci/pipeline_artifact_spec.rb114
-rw-r--r--spec/models/ci/pipeline_spec.rb107
-rw-r--r--spec/models/ci/processable_spec.rb54
-rw-r--r--spec/models/ci/resource_group_spec.rb12
-rw-r--r--spec/models/ci/resource_spec.rb2
-rw-r--r--spec/models/ci/stage_spec.rb3
-rw-r--r--spec/models/clusters/agent_spec.rb1
-rw-r--r--spec/models/clusters/agent_token_spec.rb1
-rw-r--r--spec/models/clusters/platforms/kubernetes_spec.rb10
-rw-r--r--spec/models/commit_spec.rb13
-rw-r--r--spec/models/commit_status_spec.rb73
-rw-r--r--spec/models/concerns/atomic_internal_id_spec.rb152
-rw-r--r--spec/models/concerns/bulk_insert_safe_spec.rb7
-rw-r--r--spec/models/concerns/featurable_spec.rb16
-rw-r--r--spec/models/concerns/issuable_spec.rb19
-rw-r--r--spec/models/concerns/nullify_if_blank_spec.rb51
-rw-r--r--spec/models/concerns/protected_ref_spec.rb77
-rw-r--r--spec/models/concerns/spammable_spec.rb55
-rw-r--r--spec/models/concerns/token_authenticatable_spec.rb4
-rw-r--r--spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb8
-rw-r--r--spec/models/deployment_spec.rb56
-rw-r--r--spec/models/design_management/design_spec.rb21
-rw-r--r--spec/models/event_spec.rb64
-rw-r--r--spec/models/experiment_spec.rb57
-rw-r--r--spec/models/group_spec.rb40
-rw-r--r--spec/models/issue_link_spec.rb2
-rw-r--r--spec/models/issue_spec.rb4
-rw-r--r--spec/models/license_template_spec.rb2
-rw-r--r--spec/models/merge_request/metrics_spec.rb11
-rw-r--r--spec/models/merge_request_diff_commit_spec.rb9
-rw-r--r--spec/models/merge_request_diff_spec.rb51
-rw-r--r--spec/models/merge_request_spec.rb239
-rw-r--r--spec/models/namespace_spec.rb76
-rw-r--r--spec/models/note_spec.rb38
-rw-r--r--spec/models/onboarding_progress_spec.rb83
-rw-r--r--spec/models/operations/feature_flag_spec.rb29
-rw-r--r--spec/models/packages/composer/cache_file_spec.rb32
-rw-r--r--spec/models/packages/composer/metadatum_spec.rb16
-rw-r--r--spec/models/packages/debian/group_component_file_spec.rb7
-rw-r--r--spec/models/packages/debian/group_component_spec.rb7
-rw-r--r--spec/models/packages/debian/project_component_file_spec.rb7
-rw-r--r--spec/models/packages/debian/project_component_spec.rb7
-rw-r--r--spec/models/packages/debian/publication_spec.rb47
-rw-r--r--spec/models/packages/package_spec.rb50
-rw-r--r--spec/models/packages/rubygems/metadatum_spec.rb22
-rw-r--r--spec/models/pages/lookup_path_spec.rb38
-rw-r--r--spec/models/pages/virtual_domain_spec.rb21
-rw-r--r--spec/models/pages_deployment_spec.rb40
-rw-r--r--spec/models/project_ci_cd_setting_spec.rb37
-rw-r--r--spec/models/project_services/alerts_service_spec.rb39
-rw-r--r--spec/models/project_services/chat_notification_service_spec.rb33
-rw-r--r--spec/models/project_services/confluence_service_spec.rb6
-rw-r--r--spec/models/project_services/datadog_service_spec.rb36
-rw-r--r--spec/models/project_services/jira_service_spec.rb61
-rw-r--r--spec/models/project_services/jira_tracker_data_spec.rb21
-rw-r--r--spec/models/project_spec.rb242
-rw-r--r--spec/models/project_wiki_spec.rb1
-rw-r--r--spec/models/prometheus_metric_spec.rb10
-rw-r--r--spec/models/protected_branch/push_access_level_spec.rb10
-rw-r--r--spec/models/readme_blob_spec.rb17
-rw-r--r--spec/models/release_highlight_spec.rb2
-rw-r--r--spec/models/release_spec.rb57
-rw-r--r--spec/models/repository_spec.rb78
-rw-r--r--spec/models/service_spec.rb78
-rw-r--r--spec/models/snippet_spec.rb6
-rw-r--r--spec/models/terraform/state_spec.rb12
-rw-r--r--spec/models/terraform/state_version_spec.rb18
-rw-r--r--spec/models/token_with_iv_spec.rb29
-rw-r--r--spec/models/u2f_registration_spec.rb37
-rw-r--r--spec/models/user_spec.rb75
-rw-r--r--spec/models/user_status_spec.rb30
-rw-r--r--spec/policies/project_policy_spec.rb65
-rw-r--r--spec/presenters/ci/build_runner_presenter_spec.rb14
-rw-r--r--spec/presenters/ci/pipeline_artifacts/code_quality_mr_diff_presenter_spec.rb66
-rw-r--r--spec/presenters/gitlab/whats_new/item_presenter_spec.rb29
-rw-r--r--spec/presenters/project_presenter_spec.rb2
-rw-r--r--spec/requests/api/api_spec.rb36
-rw-r--r--spec/requests/api/applications_spec.rb2
-rw-r--r--spec/requests/api/ci/pipelines_spec.rb34
-rw-r--r--spec/requests/api/debian_group_packages_spec.rb20
-rw-r--r--spec/requests/api/debian_project_packages_spec.rb28
-rw-r--r--spec/requests/api/deploy_tokens_spec.rb16
-rw-r--r--spec/requests/api/events_spec.rb6
-rw-r--r--spec/requests/api/generic_packages_spec.rb75
-rw-r--r--spec/requests/api/graphql/ci/application_setting_spec.rb49
-rw-r--r--spec/requests/api/graphql/ci/ci_cd_setting_spec.rb2
-rw-r--r--spec/requests/api/graphql/issue/issue_spec.rb14
-rw-r--r--spec/requests/api/graphql/mutations/alert_management/http_integration/update_spec.rb16
-rw-r--r--spec/requests/api/graphql/mutations/boards/lists/create_spec.rb46
-rw-r--r--spec/requests/api/graphql/mutations/ci/ci_cd_settings_update_spec.rb4
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/reviewer_rereview_spec.rb65
-rw-r--r--spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/notes/create/image_diff_note_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/notes/create/note_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/releases/create_spec.rb8
-rw-r--r--spec/requests/api/graphql/mutations/snippets/create_spec.rb36
-rw-r--r--spec/requests/api/graphql/mutations/snippets/update_spec.rb34
-rw-r--r--spec/requests/api/graphql/packages/package_composer_details_spec.rb39
-rw-r--r--spec/requests/api/graphql/packages/package_spec.rb80
-rw-r--r--spec/requests/api/graphql/project/container_repositories_spec.rb47
-rw-r--r--spec/requests/api/graphql/project/issue/designs/notes_spec.rb19
-rw-r--r--spec/requests/api/graphql/project/merge_request/pipelines_spec.rb8
-rw-r--r--spec/requests/api/graphql/project/merge_request_spec.rb8
-rw-r--r--spec/requests/api/graphql/project/merge_requests_spec.rb108
-rw-r--r--spec/requests/api/graphql/project/packages_spec.rb31
-rw-r--r--spec/requests/api/graphql/project/release_spec.rb8
-rw-r--r--spec/requests/api/graphql/project/terraform/state_spec.rb83
-rw-r--r--spec/requests/api/group_import_spec.rb4
-rw-r--r--spec/requests/api/group_labels_spec.rb54
-rw-r--r--spec/requests/api/group_packages_spec.rb1
-rw-r--r--spec/requests/api/groups_spec.rb7
-rw-r--r--spec/requests/api/internal/base_spec.rb267
-rw-r--r--spec/requests/api/internal/kubernetes_spec.rb68
-rw-r--r--spec/requests/api/jobs_spec.rb3
-rw-r--r--spec/requests/api/labels_spec.rb59
-rw-r--r--spec/requests/api/maven_packages_spec.rb22
-rw-r--r--spec/requests/api/merge_requests_spec.rb73
-rw-r--r--spec/requests/api/npm_instance_packages_spec.rb2
-rw-r--r--spec/requests/api/npm_project_packages_spec.rb34
-rw-r--r--spec/requests/api/nuget_project_packages_spec.rb14
-rw-r--r--spec/requests/api/oauth_tokens_spec.rb15
-rw-r--r--spec/requests/api/project_attributes.yml149
-rw-r--r--spec/requests/api/project_import_spec.rb5
-rw-r--r--spec/requests/api/project_packages_spec.rb1
-rw-r--r--spec/requests/api/project_templates_spec.rb2
-rw-r--r--spec/requests/api/projects_spec.rb77
-rw-r--r--spec/requests/api/pypi_packages_spec.rb27
-rw-r--r--spec/requests/api/repositories_spec.rb81
-rw-r--r--spec/requests/api/resource_access_tokens_spec.rb293
-rw-r--r--spec/requests/api/rubygem_packages_spec.rb139
-rw-r--r--spec/requests/api/settings_spec.rb18
-rw-r--r--spec/requests/api/suggestions_spec.rb27
-rw-r--r--spec/requests/api/templates_spec.rb6
-rw-r--r--spec/requests/api/terraform/state_spec.rb2
-rw-r--r--spec/requests/api/triggers_spec.rb11
-rw-r--r--spec/requests/api/users_spec.rb199
-rw-r--r--spec/requests/api/version_spec.rb12
-rw-r--r--spec/requests/git_http_spec.rb16
-rw-r--r--spec/requests/groups/email_campaigns_controller_spec.rb86
-rw-r--r--spec/requests/health_controller_spec.rb152
-rw-r--r--spec/requests/import/gitlab_groups_controller_spec.rb6
-rw-r--r--spec/requests/import/gitlab_projects_controller_spec.rb3
-rw-r--r--spec/requests/oauth/tokens_controller_spec.rb14
-rw-r--r--spec/requests/projects/ci/promeheus_metrics/histograms_controller_spec.rb46
-rw-r--r--spec/requests/projects/noteable_notes_spec.rb11
-rw-r--r--spec/requests/rack_attack_global_spec.rb6
-rw-r--r--spec/requests/users_controller_spec.rb8
-rw-r--r--spec/requests/whats_new_controller_spec.rb66
-rw-r--r--spec/routing/admin_routing_spec.rb7
-rw-r--r--spec/routing/projects/security/configuration_controller_routing_spec.rb15
-rw-r--r--spec/rubocop/code_reuse_helpers_spec.rb56
-rw-r--r--spec/rubocop/cop/active_record_association_reload_spec.rb2
-rw-r--r--spec/rubocop/cop/api/base_spec.rb5
-rw-r--r--spec/rubocop/cop/api/grape_array_missing_coerce_spec.rb28
-rw-r--r--spec/rubocop/cop/avoid_becomes_spec.rb30
-rw-r--r--spec/rubocop/cop/avoid_break_from_strong_memoize_spec.rb6
-rw-r--r--spec/rubocop/cop/avoid_keyword_arguments_in_sidekiq_workers_spec.rb7
-rw-r--r--spec/rubocop/cop/ban_catch_throw_spec.rb26
-rw-r--r--spec/rubocop/cop/code_reuse/finder_spec.rb5
-rw-r--r--spec/rubocop/cop/code_reuse/presenter_spec.rb3
-rw-r--r--spec/rubocop/cop/code_reuse/serializer_spec.rb3
-rw-r--r--spec/rubocop/cop/code_reuse/service_class_spec.rb3
-rw-r--r--spec/rubocop/cop/code_reuse/worker_spec.rb3
-rw-r--r--spec/rubocop/cop/default_scope_spec.rb39
-rw-r--r--spec/rubocop/cop/destroy_all_spec.rb39
-rw-r--r--spec/rubocop/cop/gitlab/avoid_uploaded_file_from_params_spec.rb9
-rw-r--r--spec/rubocop/cop/gitlab/bulk_insert_spec.rb11
-rw-r--r--spec/rubocop/cop/gitlab/change_timezone_spec.rb5
-rw-r--r--spec/rubocop/cop/gitlab/const_get_inherit_false_spec.rb47
-rw-r--r--spec/rubocop/cop/gitlab/duplicate_spec_location_spec.rb2
-rw-r--r--spec/rubocop/cop/gitlab/except_spec.rb3
-rw-r--r--spec/rubocop/cop/gitlab/finder_with_find_by_spec.rb49
-rw-r--r--spec/rubocop/cop/gitlab/httparty_spec.rb34
-rw-r--r--spec/rubocop/cop/gitlab/intersect_spec.rb3
-rw-r--r--spec/rubocop/cop/gitlab/json_spec.rb33
-rw-r--r--spec/rubocop/cop/gitlab/module_with_instance_variables_spec.rb35
-rw-r--r--spec/rubocop/cop/gitlab/namespaced_class_spec.rb73
-rw-r--r--spec/rubocop/cop/gitlab/policy_rule_boolean_spec.rb3
-rw-r--r--spec/rubocop/cop/gitlab/predicate_memoization_spec.rb74
-rw-r--r--spec/rubocop/cop/gitlab/rails_logger_spec.rb22
-rw-r--r--spec/rubocop/cop/gitlab/union_spec.rb3
-rw-r--r--spec/rubocop/cop/graphql/authorize_types_spec.rb7
-rw-r--r--spec/rubocop/cop/graphql/descriptions_spec.rb22
-rw-r--r--spec/rubocop/cop/graphql/gid_expected_type_spec.rb7
-rw-r--r--spec/rubocop/cop/graphql/id_type_spec.rb7
-rw-r--r--spec/rubocop/cop/graphql/json_type_spec.rb24
-rw-r--r--spec/rubocop/cop/graphql/resolver_type_spec.rb18
-rw-r--r--spec/rubocop/cop/group_public_or_visible_to_user_spec.rb23
-rw-r--r--spec/rubocop/cop/include_sidekiq_worker_spec.rb30
-rw-r--r--spec/rubocop/cop/inject_enterprise_edition_module_spec.rb24
-rw-r--r--spec/rubocop/cop/lint/last_keyword_argument_spec.rb2
-rw-r--r--spec/rubocop/cop/migration/add_limit_to_text_columns_spec.rb18
-rw-r--r--spec/rubocop/cop/performance/ar_count_each_spec.rb4
-rw-r--r--spec/rubocop/cop/performance/ar_exists_and_present_blank_spec.rb8
-rw-r--r--spec/rubocop/cop/performance/readlines_each_spec.rb23
-rw-r--r--spec/rubocop/cop/project_path_helper_spec.rb29
-rw-r--r--spec/rubocop/cop/put_project_routes_under_scope_spec.rb2
-rw-r--r--spec/rubocop/cop/qa/ambiguous_page_object_name_spec.rb4
-rw-r--r--spec/rubocop/cop/qa/element_with_pattern_spec.rb4
-rw-r--r--spec/rubocop/cop/rspec/any_instance_of_spec.rb40
-rw-r--r--spec/rubocop/cop/rspec/be_success_matcher_spec.rb27
-rw-r--r--spec/rubocop/cop/rspec/env_assignment_spec.rb33
-rw-r--r--spec/rubocop/cop/rspec/expect_gitlab_tracking_spec.rb27
-rw-r--r--spec/rubocop/cop/rspec/factories_in_migration_specs_spec.rb6
-rw-r--r--spec/rubocop/cop/rspec/factory_bot/inline_association_spec.rb30
-rw-r--r--spec/rubocop/cop/rspec/have_gitlab_http_status_spec.rb51
-rw-r--r--spec/rubocop/cop/rspec/htt_party_basic_auth_spec.rb18
-rw-r--r--spec/rubocop/cop/rspec/modify_sidekiq_middleware_spec.rb39
-rw-r--r--spec/rubocop/cop/rspec/timecop_freeze_spec.rb47
-rw-r--r--spec/rubocop/cop/rspec/timecop_travel_spec.rb47
-rw-r--r--spec/rubocop/cop/rspec/top_level_describe_path_spec.rb2
-rw-r--r--spec/rubocop/cop/ruby_interpolation_in_translation_spec.rb67
-rw-r--r--spec/rubocop/cop/safe_params_spec.rb3
-rw-r--r--spec/rubocop/cop/scalability/bulk_perform_with_context_spec.rb24
-rw-r--r--spec/rubocop/cop/scalability/cron_worker_context_spec.rb7
-rw-r--r--spec/rubocop/cop/scalability/file_uploads_spec.rb2
-rw-r--r--spec/rubocop/cop/scalability/idempotent_worker_spec.rb11
-rw-r--r--spec/rubocop/cop/static_translation_definition_spec.rb98
-rw-r--r--spec/rubocop/cop/usage_data/distinct_count_by_large_foreign_key_spec.rb42
-rw-r--r--spec/rubocop/cop/usage_data/large_table_spec.rb56
-rw-r--r--spec/rubocop/qa_helpers_spec.rb6
-rw-r--r--spec/serializers/admin/user_entity_spec.rb1
-rw-r--r--spec/serializers/admin/user_serializer_spec.rb1
-rw-r--r--spec/serializers/ci/codequality_mr_diff_entity_spec.rb27
-rw-r--r--spec/serializers/ci/codequality_mr_diff_report_serializer_spec.rb32
-rw-r--r--spec/serializers/ci/dag_pipeline_entity_spec.rb6
-rw-r--r--spec/serializers/ci/pipeline_entity_spec.rb261
-rw-r--r--spec/serializers/codequality_degradation_entity_spec.rb63
-rw-r--r--spec/serializers/codequality_reports_comparer_entity_spec.rb58
-rw-r--r--spec/serializers/codequality_reports_comparer_serializer_spec.rb58
-rw-r--r--spec/serializers/diff_file_metadata_entity_spec.rb27
-rw-r--r--spec/serializers/diffs_entity_spec.rb15
-rw-r--r--spec/serializers/group_link/group_group_link_entity_spec.rb (renamed from spec/serializers/group_group_link_entity_spec.rb)8
-rw-r--r--spec/serializers/group_link/group_group_link_serializer_spec.rb (renamed from spec/serializers/group_group_link_serializer_spec.rb)4
-rw-r--r--spec/serializers/group_link/group_link_entity_spec.rb25
-rw-r--r--spec/serializers/group_link/project_group_link_entity_spec.rb30
-rw-r--r--spec/serializers/group_link/project_group_link_serializer_spec.rb13
-rw-r--r--spec/serializers/member_entity_spec.rb36
-rw-r--r--spec/serializers/member_serializer_spec.rb4
-rw-r--r--spec/serializers/merge_request_basic_entity_spec.rb22
-rw-r--r--spec/serializers/merge_request_sidebar_extras_entity_spec.rb22
-rw-r--r--spec/serializers/merge_request_user_entity_spec.rb18
-rw-r--r--spec/serializers/merge_request_widget_entity_spec.rb6
-rw-r--r--spec/serializers/paginated_diff_entity_spec.rb2
-rw-r--r--spec/serializers/pipeline_details_entity_spec.rb4
-rw-r--r--spec/serializers/pipeline_entity_spec.rb264
-rw-r--r--spec/serializers/user_serializer_spec.rb2
-rw-r--r--spec/services/alert_management/process_prometheus_alert_service_spec.rb47
-rw-r--r--spec/services/application_settings/update_service_spec.rb2
-rw-r--r--spec/services/authorized_project_update/recalculate_for_user_range_service_spec.rb2
-rw-r--r--spec/services/boards/lists/create_service_spec.rb104
-rw-r--r--spec/services/bulk_create_integration_service_spec.rb51
-rw-r--r--spec/services/captcha/captcha_verification_service_spec.rb39
-rw-r--r--spec/services/ci/create_job_artifacts_service_spec.rb28
-rw-r--r--spec/services/ci/create_pipeline_service/cross_project_pipeline_spec.rb86
-rw-r--r--spec/services/ci/create_pipeline_service/custom_yaml_tags_spec.rb79
-rw-r--r--spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb50
-rw-r--r--spec/services/ci/create_pipeline_service/rules_spec.rb14
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb7
-rw-r--r--spec/services/ci/daily_build_group_report_result_service_spec.rb28
-rw-r--r--spec/services/ci/generate_codequality_mr_diff_report_service_spec.rb51
-rw-r--r--spec/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service_spec.rb62
-rw-r--r--spec/services/ci/pipeline_trigger_service_spec.rb29
-rw-r--r--spec/services/ci/process_build_service_spec.rb6
-rw-r--r--spec/services/ci/process_pipeline_service_spec.rb29
-rw-r--r--spec/services/ci/prometheus_metrics/observe_histograms_service_spec.rb86
-rw-r--r--spec/services/ci/register_job_service_spec.rb22
-rw-r--r--spec/services/container_expiration_policies/cleanup_service_spec.rb22
-rw-r--r--spec/services/deployments/create_service_spec.rb21
-rw-r--r--spec/services/design_management/move_designs_service_spec.rb12
-rw-r--r--spec/services/discussions/resolve_service_spec.rb14
-rw-r--r--spec/services/discussions/unresolve_service_spec.rb32
-rw-r--r--spec/services/feature_flags/create_service_spec.rb12
-rw-r--r--spec/services/feature_flags/update_service_spec.rb12
-rw-r--r--spec/services/git/branch_hooks_service_spec.rb182
-rw-r--r--spec/services/git/wiki_push_service_spec.rb40
-rw-r--r--spec/services/groups/import_export/export_service_spec.rb14
-rw-r--r--spec/services/groups/open_issues_count_service_spec.rb106
-rw-r--r--spec/services/integrations/test/project_service_spec.rb50
-rw-r--r--spec/services/issue_rebalancing_service_spec.rb108
-rw-r--r--spec/services/issues/close_service_spec.rb3
-rw-r--r--spec/services/issues/create_service_spec.rb176
-rw-r--r--spec/services/issues/update_service_spec.rb2
-rw-r--r--spec/services/members/update_service_spec.rb24
-rw-r--r--spec/services/merge_requests/after_create_service_spec.rb13
-rw-r--r--spec/services/merge_requests/approval_service_spec.rb14
-rw-r--r--spec/services/merge_requests/build_service_spec.rb2
-rw-r--r--spec/services/merge_requests/create_from_issue_service_spec.rb19
-rw-r--r--spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb2
-rw-r--r--spec/services/merge_requests/mark_reviewer_reviewed_service_spec.rb45
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb14
-rw-r--r--spec/services/merge_requests/mergeability_check_service_spec.rb28
-rw-r--r--spec/services/merge_requests/post_merge_service_spec.rb142
-rw-r--r--spec/services/merge_requests/push_options_handler_service_spec.rb45
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb41
-rw-r--r--spec/services/merge_requests/reload_merge_head_diff_service_spec.rb61
-rw-r--r--spec/services/merge_requests/remove_approval_service_spec.rb14
-rw-r--r--spec/services/merge_requests/request_review_service_spec.rb69
-rw-r--r--spec/services/merge_requests/update_service_spec.rb95
-rw-r--r--spec/services/namespaces/in_product_marketing_emails_service_spec.rb159
-rw-r--r--spec/services/notes/create_service_spec.rb20
-rw-r--r--spec/services/notification_recipients/build_service_spec.rb24
-rw-r--r--spec/services/notification_service_spec.rb40
-rw-r--r--spec/services/packages/composer/create_package_service_spec.rb2
-rw-r--r--spec/services/packages/conan/create_package_service_spec.rb1
-rw-r--r--spec/services/packages/debian/create_distribution_service_spec.rb122
-rw-r--r--spec/services/packages/debian/destroy_distribution_service_spec.rb78
-rw-r--r--spec/services/packages/debian/update_distribution_service_spec.rb159
-rw-r--r--spec/services/packages/generic/create_package_file_service_spec.rb40
-rw-r--r--spec/services/packages/maven/find_or_create_package_service_spec.rb10
-rw-r--r--spec/services/packages/npm/create_package_service_spec.rb1
-rw-r--r--spec/services/packages/nuget/create_package_service_spec.rb1
-rw-r--r--spec/services/packages/pypi/create_package_service_spec.rb1
-rw-r--r--spec/services/pages/delete_services_spec.rb81
-rw-r--r--spec/services/pages/migrate_from_legacy_storage_service_spec.rb92
-rw-r--r--spec/services/pages/migrate_legacy_storage_to_deployment_service_spec.rb8
-rw-r--r--spec/services/pages/zip_directory_service_spec.rb83
-rw-r--r--spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb2
-rw-r--r--spec/services/post_receive_service_spec.rb14
-rw-r--r--spec/services/projects/alerting/notify_service_spec.rb13
-rw-r--r--spec/services/projects/branches_by_mode_service_spec.rb136
-rw-r--r--spec/services/projects/cleanup_service_spec.rb2
-rw-r--r--spec/services/projects/container_repository/cleanup_tags_service_spec.rb4
-rw-r--r--spec/services/projects/create_service_spec.rb44
-rw-r--r--spec/services/projects/fork_service_spec.rb44
-rw-r--r--spec/services/projects/prometheus/alerts/notify_service_spec.rb7
-rw-r--r--spec/services/projects/update_pages_service_spec.rb11
-rw-r--r--spec/services/projects/update_repository_storage_service_spec.rb13
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb194
-rw-r--r--spec/services/repositories/changelog_service_spec.rb130
-rw-r--r--spec/services/resource_access_tokens/create_service_spec.rb10
-rw-r--r--spec/services/resource_access_tokens/revoke_service_spec.rb8
-rw-r--r--spec/services/resource_events/change_milestone_service_spec.rb4
-rw-r--r--spec/services/search/global_service_spec.rb22
-rw-r--r--spec/services/search/group_service_spec.rb24
-rw-r--r--spec/services/security/ci_configuration/sast_create_service_spec.rb69
-rw-r--r--spec/services/security/ci_configuration/sast_parser_service_spec.rb76
-rw-r--r--spec/services/snippets/create_service_spec.rb5
-rw-r--r--spec/services/snippets/update_repository_storage_service_spec.rb13
-rw-r--r--spec/services/snippets/update_service_spec.rb13
-rw-r--r--spec/services/spam/spam_action_service_spec.rb182
-rw-r--r--spec/services/suggestions/apply_service_spec.rb104
-rw-r--r--spec/services/suggestions/create_service_spec.rb33
-rw-r--r--spec/services/system_hooks_service_spec.rb9
-rw-r--r--spec/services/system_note_service_spec.rb11
-rw-r--r--spec/services/system_notes/issuables_service_spec.rb2
-rw-r--r--spec/services/system_notes/merge_requests_service_spec.rb28
-rw-r--r--spec/services/test_hooks/project_service_spec.rb55
-rw-r--r--spec/services/test_hooks/system_service_spec.rb17
-rw-r--r--spec/services/todo_service_spec.rb11
-rw-r--r--spec/services/users/approve_service_spec.rb24
-rw-r--r--spec/services/users/batch_status_cleaner_service_spec.rb43
-rw-r--r--spec/services/users/build_service_spec.rb6
-rw-r--r--spec/services/users/refresh_authorized_projects_service_spec.rb15
-rw-r--r--spec/services/users/reject_service_spec.rb20
-rw-r--r--spec/spec_helper.rb14
-rw-r--r--spec/support/factory_bot.rb13
-rw-r--r--spec/support/gitlab_experiment.rb12
-rw-r--r--spec/support/graphql/field_selection.rb2
-rw-r--r--spec/support/helpers/cycle_analytics_helpers.rb26
-rw-r--r--spec/support/helpers/features/members_table_helpers.rb26
-rw-r--r--spec/support/helpers/features/notes_helpers.rb4
-rw-r--r--spec/support/helpers/graphql_helpers.rb47
-rw-r--r--spec/support/helpers/next_found_instance_of.rb16
-rw-r--r--spec/support/helpers/search_settings_helpers.rb5
-rw-r--r--spec/support/helpers/seed_helper.rb2
-rw-r--r--spec/support/helpers/smime_helper.rb2
-rw-r--r--spec/support/helpers/sorting_helper.rb31
-rw-r--r--spec/support/helpers/stub_configuration.rb6
-rw-r--r--spec/support/helpers/stub_object_storage.rb7
-rw-r--r--spec/support/helpers/test_env.rb4
-rw-r--r--spec/support/helpers/wait_for_requests.rb2
-rw-r--r--spec/support/matchers/markdown_matchers.rb27
-rw-r--r--spec/support/matchers/nullify_if_blank_matcher.rb11
-rw-r--r--spec/support/matchers/pushed_frontend_feature_flags_matcher.rb23
-rw-r--r--spec/support/matchers/track_self_describing_event_matcher.rb12
-rw-r--r--spec/support/memory_instrumentation_helper.rb17
-rw-r--r--spec/support/migrations_helpers/vulnerabilities_findings_helper.rb118
-rw-r--r--spec/support/shared_contexts/navbar_structure_context.rb7
-rw-r--r--spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb8
-rw-r--r--spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb15
-rw-r--r--spec/support/shared_contexts/requests/api/workhorse_shared_context.rb6
-rw-r--r--spec/support/shared_contexts/services_shared_context.rb1
-rw-r--r--spec/support/shared_examples/alert_notification_service_shared_examples.rb18
-rw-r--r--spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/controllers/unique_hll_events_examples.rb18
-rw-r--r--spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/comment_and_close_button_shared_examples.rb29
-rw-r--r--spec/support/shared_examples/features/discussion_comments_shared_example.rb3
-rw-r--r--spec/support/shared_examples/features/editable_merge_request_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/multiple_reviewers_mr_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/navbar_shared_examples.rb5
-rw-r--r--spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/protected_branches_with_deploy_keys_examples.rb3
-rw-r--r--spec/support/shared_examples/features/search_settings_shared_examples.rb42
-rw-r--r--spec/support/shared_examples/finders/packages_shared_examples.rb20
-rw-r--r--spec/support/shared_examples/graphql/label_fields.rb4
-rw-r--r--spec/support/shared_examples/graphql/mutations/boards_list_create_shared_examples.rb83
-rw-r--r--spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb36
-rw-r--r--spec/support/shared_examples/graphql/mutations/http_integrations_shared_examples.rb75
-rw-r--r--spec/support/shared_examples/graphql/mutations/spammable_mutation_fields_examples.rb50
-rw-r--r--spec/support/shared_examples/graphql/notes_creation_shared_examples.rb19
-rw-r--r--spec/support/shared_examples/helpers/issuable_description_templates_shared_examples.rb49
-rw-r--r--spec/support/shared_examples/lib/api/internal_base_shared_examples.rb36
-rw-r--r--spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb24
-rw-r--r--spec/support/shared_examples/lib/gitlab/usage_data_counters/issue_activity_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb95
-rw-r--r--spec/support/shared_examples/models/concerns/can_housekeep_repository_shared_examples.rb45
-rw-r--r--spec/support/shared_examples/models/concerns/can_move_repository_storage_shared_examples.rb11
-rw-r--r--spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb32
-rw-r--r--spec/support/shared_examples/models/concerns/repositories/can_housekeep_repository_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb11
-rw-r--r--spec/support/shared_examples/models/packages/debian/architecture_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb245
-rw-r--r--spec/support/shared_examples/models/packages/debian/component_shared_examples.rb51
-rw-r--r--spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/models/wiki_shared_examples.rb9
-rw-r--r--spec/support/shared_examples/models/with_debian_distributions_shared_examples.rb24
-rw-r--r--spec/support/shared_examples/namespaces/recursive_traversal_examples.rb78
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/quick_actions/issue/clone_quick_action_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/quick_actions/issue/zoom_quick_actions_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb43
-rw-r--r--spec/support/shared_examples/requests/api/graphql/mutations/create_list_shared_examples.rb51
-rw-r--r--spec/support/shared_examples/requests/api/graphql/noteable_shared_examples.rb62
-rw-r--r--spec/support/shared_examples/requests/api/notes_shared_examples.rb29
-rw-r--r--spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb490
-rw-r--r--spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb190
-rw-r--r--spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/packages_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/requests/api/packages_tags_shared_examples.rb185
-rw-r--r--spec/support/shared_examples/requests/api/read_user_shared_examples.rb32
-rw-r--r--spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb44
-rw-r--r--spec/support/shared_examples/requests/api/resolvable_discussions_shared_examples.rb3
-rw-r--r--spec/support/shared_examples/requests/rack_attack_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/services/boards/boards_list_service_shared_examples.rb28
-rw-r--r--spec/support/shared_examples/services/boards/lists_create_service_shared_examples.rb81
-rw-r--r--spec/support/shared_examples/services/issuable_shared_examples.rb21
-rw-r--r--spec/support/shared_examples/services/packages_shared_examples.rb52
-rw-r--r--spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb19
-rw-r--r--spec/support/shared_examples/services/repositories/housekeeping_shared_examples.rb18
-rw-r--r--spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/services/snippets_shared_examples.rb68
-rw-r--r--spec/support/shared_examples/workers/concerns/git_garbage_collect_methods_shared_examples.rb320
-rw-r--r--spec/tasks/gitlab/cleanup_rake_spec.rb22
-rw-r--r--spec/tasks/gitlab/db_rake_spec.rb51
-rw-r--r--spec/tasks/gitlab/git_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/pages_rake_spec.rb94
-rw-r--r--spec/tasks/gitlab/password_rake_spec.rb76
-rw-r--r--spec/tasks/gitlab/terraform/migrate_rake_spec.rb45
-rw-r--r--spec/tooling/danger/base_linter_spec.rb (renamed from spec/lib/gitlab/danger/base_linter_spec.rb)5
-rw-r--r--spec/tooling/danger/changelog_spec.rb (renamed from spec/lib/gitlab/danger/changelog_spec.rb)9
-rw-r--r--spec/tooling/danger/commit_linter_spec.rb (renamed from spec/lib/gitlab/danger/commit_linter_spec.rb)5
-rw-r--r--spec/tooling/danger/danger_spec_helper.rb (renamed from spec/lib/gitlab/danger/danger_spec_helper.rb)0
-rw-r--r--spec/tooling/danger/emoji_checker_spec.rb (renamed from spec/lib/gitlab/danger/emoji_checker_spec.rb)5
-rw-r--r--spec/tooling/danger/feature_flag_spec.rb157
-rw-r--r--spec/tooling/danger/helper_spec.rb (renamed from spec/lib/gitlab/danger/helper_spec.rb)120
-rw-r--r--spec/tooling/danger/merge_request_linter_spec.rb (renamed from spec/lib/gitlab/danger/merge_request_linter_spec.rb)5
-rw-r--r--spec/tooling/danger/roulette_spec.rb (renamed from spec/lib/gitlab/danger/roulette_spec.rb)48
-rw-r--r--spec/tooling/danger/sidekiq_queues_spec.rb (renamed from spec/lib/gitlab/danger/sidekiq_queues_spec.rb)5
-rw-r--r--spec/tooling/danger/teammate_spec.rb (renamed from spec/lib/gitlab/danger/teammate_spec.rb)15
-rw-r--r--spec/tooling/danger/title_linting_spec.rb (renamed from spec/lib/gitlab/danger/title_linting_spec.rb)41
-rw-r--r--spec/tooling/danger/weightage/maintainers_spec.rb (renamed from spec/lib/gitlab/danger/weightage/maintainers_spec.rb)10
-rw-r--r--spec/tooling/danger/weightage/reviewers_spec.rb (renamed from spec/lib/gitlab/danger/weightage/reviewers_spec.rb)14
-rw-r--r--spec/tooling/gitlab_danger_spec.rb (renamed from spec/lib/gitlab_danger_spec.rb)2
-rw-r--r--spec/tooling/lib/tooling/kubernetes_client_spec.rb8
-rw-r--r--spec/uploaders/packages/composer/cache_uploader_spec.rb45
-rw-r--r--spec/uploaders/packages/debian/component_file_uploader_spec.rb52
-rw-r--r--spec/validators/nested_attributes_duplicates_validator_spec.rb (renamed from spec/validators/variable_duplicates_validator_spec.rb)50
-rw-r--r--spec/views/groups/show.html.haml_spec.rb52
-rw-r--r--spec/views/layouts/_head.html.haml_spec.rb16
-rw-r--r--spec/views/layouts/header/_new_dropdown.haml_spec.rb6
-rw-r--r--spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb2
-rw-r--r--spec/views/layouts/nav/sidebar/_project_security_link.html.haml_spec.rb29
-rw-r--r--spec/views/layouts/profile.html.haml_spec.rb39
-rw-r--r--spec/views/projects/_home_panel.html.haml_spec.rb1
-rw-r--r--spec/views/projects/empty.html.haml_spec.rb37
-rw-r--r--spec/views/projects/show.html.haml_spec.rb51
-rw-r--r--spec/views/projects/tree/_tree_row.html.haml_spec.rb43
-rw-r--r--spec/views/registrations/welcome/show.html.haml_spec.rb1
-rw-r--r--spec/views/search/_filter.html.haml_spec.rb17
-rw-r--r--spec/views/search/_form.html.haml_spec.rb14
-rw-r--r--spec/views/shared/ssh_keys/_key_details.html.haml_spec.rb20
-rw-r--r--spec/workers/build_coverage_worker_spec.rb25
-rw-r--r--spec/workers/build_finished_worker_spec.rb20
-rw-r--r--spec/workers/build_trace_sections_worker_spec.rb25
-rw-r--r--spec/workers/bulk_import_worker_spec.rb15
-rw-r--r--spec/workers/bulk_imports/entity_worker_spec.rb14
-rw-r--r--spec/workers/ci/build_report_result_worker_spec.rb30
-rw-r--r--spec/workers/ci/pipeline_artifacts/create_quality_report_worker_spec.rb40
-rw-r--r--spec/workers/ci/pipeline_artifacts/expire_artifacts_worker_spec.rb2
-rw-r--r--spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb10
-rw-r--r--spec/workers/git_garbage_collect_worker_spec.rb346
-rw-r--r--spec/workers/jira_connect/sync_builds_worker_spec.rb24
-rw-r--r--spec/workers/jira_connect/sync_deployments_worker_spec.rb24
-rw-r--r--spec/workers/jira_connect/sync_feature_flags_worker_spec.rb24
-rw-r--r--spec/workers/merge_request_cleanup_refs_worker_spec.rb12
-rw-r--r--spec/workers/namespaceless_project_destroy_worker_spec.rb2
-rw-r--r--spec/workers/namespaces/in_product_marketing_emails_worker_spec.rb29
-rw-r--r--spec/workers/namespaces/onboarding_pipeline_created_worker_spec.rb25
-rw-r--r--spec/workers/namespaces/onboarding_user_added_worker_spec.rb17
-rw-r--r--spec/workers/new_note_worker_spec.rb20
-rw-r--r--spec/workers/packages/composer/cache_cleanup_worker_spec.rb29
-rw-r--r--spec/workers/pages_remove_worker_spec.rb21
-rw-r--r--spec/workers/pages_transfer_worker_spec.rb2
-rw-r--r--spec/workers/post_receive_spec.rb8
-rw-r--r--spec/workers/projects/git_garbage_collect_worker_spec.rb78
-rw-r--r--spec/workers/schedule_merge_request_cleanup_refs_worker_spec.rb12
-rw-r--r--spec/workers/user_status_cleanup/batch_worker_spec.rb33
-rw-r--r--spec/workers/wikis/git_garbage_collect_worker_spec.rb14
2245 files changed, 49305 insertions, 19949 deletions
diff --git a/spec/benchmarks/banzai_benchmark.rb b/spec/benchmarks/banzai_benchmark.rb
new file mode 100644
index 00000000000..a87414ba512
--- /dev/null
+++ b/spec/benchmarks/banzai_benchmark.rb
@@ -0,0 +1,124 @@
+# frozen_string_literal: true
+
+return unless ENV.key?('BENCHMARK')
+
+require 'spec_helper'
+require 'erb'
+require 'benchmark/ips'
+
+# This benchmarks some of the Banzai pipelines and filters.
+# They are not definitive, but can be used by a developer to
+# get a rough idea how the changing or addition of a new filter
+# will effect performance.
+#
+# Run by:
+# BENCHMARK=1 rspec spec/benchmarks/banzai_benchmark.rb
+# or
+# rake benchmark:banzai
+#
+# rubocop: disable RSpec/TopLevelDescribePath
+RSpec.describe 'GitLab Markdown Benchmark', :aggregate_failures do
+ include MarkupHelper
+
+ let_it_be(:feature) { MarkdownFeature.new }
+ let_it_be(:project) { feature.project }
+ let_it_be(:group) { feature.group }
+ let_it_be(:wiki) { feature.wiki }
+ let_it_be(:wiki_page) { feature.wiki_page }
+ let_it_be(:markdown_text) { feature.raw_markdown }
+ let_it_be(:grafana_integration) { create(:grafana_integration, project: project) }
+ let_it_be(:default_context) do
+ {
+ project: project,
+ current_user: current_user,
+ suggestions_filter_enabled: true
+ }
+ end
+
+ let(:context) do
+ Banzai::Filter::AssetProxyFilter.transform_context(default_context)
+ end
+
+ let!(:render_context) { Banzai::RenderContext.new(project, current_user) }
+
+ before do
+ stub_application_setting(asset_proxy_enabled: true)
+ stub_application_setting(asset_proxy_secret_key: 'shared-secret')
+ stub_application_setting(asset_proxy_url: 'https://assets.example.com')
+ stub_application_setting(asset_proxy_whitelist: %w(gitlab.com *.mydomain.com))
+ stub_application_setting(plantuml_enabled: true, plantuml_url: 'http://localhost:8080')
+ stub_application_setting(kroki_enabled: true, kroki_url: 'http://localhost:8000')
+
+ Banzai::Filter::AssetProxyFilter.initialize_settings
+ end
+
+ context 'pipelines' do
+ it 'benchmarks several pipelines' do
+ path = 'images/example.jpg'
+ gitaly_wiki_file = Gitlab::GitalyClient::WikiFile.new(path: path)
+ allow(wiki).to receive(:find_file).with(path).and_return(Gitlab::Git::WikiFile.new(gitaly_wiki_file))
+ allow(wiki).to receive(:wiki_base_path) { '/namespace1/gitlabhq/wikis' }
+
+ puts "\n--> Benchmarking Full, Wiki, and Plain pipelines\n"
+
+ Benchmark.ips do |x|
+ x.config(time: 10, warmup: 2)
+
+ x.report('Full pipeline') { Banzai::Pipeline::FullPipeline.call(markdown_text, context) }
+ x.report('Wiki pipeline') { Banzai::Pipeline::WikiPipeline.call(markdown_text, context.merge(wiki: wiki, page_slug: wiki_page.slug)) }
+ x.report('Plain pipeline') { Banzai::Pipeline::PlainMarkdownPipeline.call(markdown_text, context) }
+
+ x.compare!
+ end
+ end
+ end
+
+ context 'filters' do
+ it 'benchmarks all filters in the FullPipeline' do
+ benchmark_pipeline_filters(:full)
+ end
+
+ it 'benchmarks all filters in the PlainMarkdownPipeline' do
+ benchmark_pipeline_filters(:plain_markdown)
+ end
+ end
+
+ # build up the source text for each filter
+ def build_filter_text(pipeline, initial_text)
+ filter_source = {}
+ input_text = initial_text
+
+ pipeline.filters.each do |filter_klass|
+ filter_source[filter_klass] = input_text
+
+ output = filter_klass.call(input_text, context)
+ input_text = output
+ end
+
+ filter_source
+ end
+
+ def benchmark_pipeline_filters(pipeline_type)
+ pipeline = Banzai::Pipeline[pipeline_type]
+ filter_source = build_filter_text(pipeline, markdown_text)
+
+ puts "\n--> Benchmarking #{pipeline.name.demodulize} filters\n"
+
+ Benchmark.ips do |x|
+ x.config(time: 10, warmup: 2)
+
+ pipeline.filters.each do |filter_klass|
+ label = filter_klass.name.demodulize.delete_suffix('Filter').truncate(20)
+
+ x.report(label) { filter_klass.call(filter_source[filter_klass], context) }
+ end
+
+ x.compare!
+ end
+ end
+
+ # Fake a `current_user` helper
+ def current_user
+ feature.user
+ end
+end
diff --git a/spec/config/object_store_settings_spec.rb b/spec/config/object_store_settings_spec.rb
index 9e7dfa043c3..68b37197ca7 100644
--- a/spec/config/object_store_settings_spec.rb
+++ b/spec/config/object_store_settings_spec.rb
@@ -49,6 +49,20 @@ RSpec.describe ObjectStoreSettings do
}
end
+ shared_examples 'consolidated settings for objects accelerated by Workhorse' do
+ it 'consolidates active object storage settings' do
+ described_class::WORKHORSE_ACCELERATED_TYPES.each do |object_type|
+ # Use to_h to avoid https://gitlab.com/gitlab-org/gitlab/-/issues/286873
+ section = subject.try(object_type).to_h
+
+ next unless section.dig('object_store', 'enabled')
+
+ expect(section['object_store']['connection']).to eq(connection)
+ expect(section['object_store']['consolidated_settings']).to be true
+ end
+ end
+ end
+
it 'sets correct default values' do
subject
@@ -77,9 +91,7 @@ RSpec.describe ObjectStoreSettings do
expect(settings.pages['object_store']['consolidated_settings']).to be true
expect(settings.external_diffs['enabled']).to be false
- expect(settings.external_diffs['object_store']['enabled']).to be false
- expect(settings.external_diffs['object_store']['remote_directory']).to eq('external_diffs')
- expect(settings.external_diffs['object_store']['consolidated_settings']).to be true
+ expect(settings.external_diffs['object_store']).to be_nil
end
it 'raises an error when a bucket is missing' do
@@ -95,29 +107,50 @@ RSpec.describe ObjectStoreSettings do
expect(settings.pages['object_store']).to eq(nil)
end
- it 'allows pages to define its own connection' do
- pages_connection = { 'provider' => 'Google', 'google_application_default' => true }
- config['pages'] = {
- 'enabled' => true,
- 'object_store' => {
+ context 'GitLab Pages' do
+ let(:pages_connection) { { 'provider' => 'Google', 'google_application_default' => true } }
+
+ before do
+ config['pages'] = {
'enabled' => true,
- 'connection' => pages_connection
+ 'object_store' => {
+ 'enabled' => true,
+ 'connection' => pages_connection
+ }
}
- }
+ end
- expect { subject }.not_to raise_error
+ it_behaves_like 'consolidated settings for objects accelerated by Workhorse'
- described_class::WORKHORSE_ACCELERATED_TYPES.each do |object_type|
- section = settings.try(object_type)
+ it 'allows pages to define its own connection' do
+ expect { subject }.not_to raise_error
- next unless section
+ expect(settings.pages['object_store']['connection']).to eq(pages_connection)
+ expect(settings.pages['object_store']['consolidated_settings']).to be_falsey
+ end
+ end
- expect(section['object_store']['connection']).to eq(connection)
- expect(section['object_store']['consolidated_settings']).to be true
+ context 'when object storage is disabled for artifacts with no bucket' do
+ before do
+ config['artifacts'] = {
+ 'enabled' => true,
+ 'object_store' => {}
+ }
+ config['object_store']['objects']['artifacts'] = {
+ 'enabled' => false
+ }
end
- expect(settings.pages['object_store']['connection']).to eq(pages_connection)
- expect(settings.pages['object_store']['consolidated_settings']).to be_falsey
+ it_behaves_like 'consolidated settings for objects accelerated by Workhorse'
+
+ it 'does not enable consolidated settings for artifacts' do
+ subject
+
+ expect(settings.artifacts['enabled']).to be true
+ expect(settings.artifacts['object_store']['remote_directory']).to be_nil
+ expect(settings.artifacts['object_store']['enabled']).to be_falsey
+ expect(settings.artifacts['object_store']['consolidated_settings']).to be_falsey
+ end
end
context 'with legacy config' do
diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb
index f0b224484c6..71abf3191b8 100644
--- a/spec/controllers/admin/application_settings_controller_spec.rb
+++ b/spec/controllers/admin/application_settings_controller_spec.rb
@@ -150,6 +150,13 @@ RSpec.describe Admin::ApplicationSettingsController do
expect(ApplicationSetting.current.repository_storages_weighted_default).to eq(75)
end
+ it 'updates kroki_formats setting' do
+ put :update, params: { application_setting: { kroki_formats_excalidraw: '1' } }
+
+ expect(response).to redirect_to(general_admin_application_settings_path)
+ expect(ApplicationSetting.current.kroki_formats_excalidraw).to eq(true)
+ end
+
it "updates default_branch_name setting" do
put :update, params: { application_setting: { default_branch_name: "example_branch_name" } }
diff --git a/spec/controllers/admin/cohorts_controller_spec.rb b/spec/controllers/admin/cohorts_controller_spec.rb
index 9eb2a713517..77a9c8eb223 100644
--- a/spec/controllers/admin/cohorts_controller_spec.rb
+++ b/spec/controllers/admin/cohorts_controller_spec.rb
@@ -3,37 +3,15 @@
require 'spec_helper'
RSpec.describe Admin::CohortsController do
- context 'as admin' do
- let(:user) { create(:admin) }
+ let(:user) { create(:admin) }
- before do
- sign_in(user)
- end
-
- it 'renders 200' do
- get :index
-
- expect(response).to have_gitlab_http_status(:success)
- end
-
- describe 'GET #index' do
- it_behaves_like 'tracking unique visits', :index do
- let(:target_id) { 'i_analytics_cohorts' }
- end
- end
+ before do
+ sign_in(user)
end
- context 'as normal user' do
- let(:user) { create(:user) }
-
- before do
- sign_in(user)
- end
-
- it 'renders a 404' do
- get :index
+ it 'redirects to Overview->Users' do
+ get :index
- expect(response).to have_gitlab_http_status(:not_found)
- end
+ expect(response).to redirect_to(admin_users_path(tab: 'cohorts'))
end
end
diff --git a/spec/controllers/admin/runners_controller_spec.rb b/spec/controllers/admin/runners_controller_spec.rb
index 3fffc50475c..cba25dbff95 100644
--- a/spec/controllers/admin/runners_controller_spec.rb
+++ b/spec/controllers/admin/runners_controller_spec.rb
@@ -27,7 +27,8 @@ RSpec.describe Admin::RunnersController do
# There is still an N+1 query for `runner.builds.count`
# We also need to add 1 because it takes 2 queries to preload tags
- expect { get :index }.not_to exceed_query_limit(control_count + 6)
+ # also looking for token nonce requires database queries
+ expect { get :index }.not_to exceed_query_limit(control_count + 16)
expect(response).to have_gitlab_http_status(:ok)
expect(response.body).to have_content('tag1')
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index f902a3d2541..6faec315eb6 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -29,6 +29,11 @@ RSpec.describe Admin::UsersController do
expect(assigns(:users).first.association(:authorized_projects)).to be_loaded
end
+
+ it_behaves_like 'tracking unique visits', :index do
+ let(:target_id) { 'i_analytics_cohorts' }
+ let(:request_params) { { tab: 'cohorts' } }
+ end
end
describe 'GET :id' do
@@ -180,7 +185,7 @@ RSpec.describe Admin::UsersController do
it 'displays the error' do
subject
- expect(flash[:alert]).to eq('The user you are trying to approve is not pending an approval')
+ expect(flash[:alert]).to eq('The user you are trying to approve is not pending approval')
end
it 'does not activate the user' do
diff --git a/spec/controllers/chaos_controller_spec.rb b/spec/controllers/chaos_controller_spec.rb
index 550303d292a..cb4f12ff829 100644
--- a/spec/controllers/chaos_controller_spec.rb
+++ b/spec/controllers/chaos_controller_spec.rb
@@ -124,4 +124,23 @@ RSpec.describe ChaosController do
expect(response).to have_gitlab_http_status(:ok)
end
end
+
+ describe '#gc' do
+ let(:gc_stat) { GC.stat.stringify_keys }
+
+ it 'runs a full GC on the current web worker' do
+ expect(Prometheus::PidProvider).to receive(:worker_id).and_return('worker-0')
+ expect(Gitlab::Chaos).to receive(:run_gc).and_return(gc_stat)
+
+ post :gc
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response_json['worker_id']).to eq('worker-0')
+ expect(response_json['gc_stat']).to eq(gc_stat)
+ end
+ end
+
+ def response_json
+ Gitlab::Json.parse(response.body)
+ end
end
diff --git a/spec/controllers/concerns/redis_tracking_spec.rb b/spec/controllers/concerns/redis_tracking_spec.rb
index ef59adf8c1d..53b49dd30a6 100644
--- a/spec/controllers/concerns/redis_tracking_spec.rb
+++ b/spec/controllers/concerns/redis_tracking_spec.rb
@@ -3,18 +3,13 @@
require "spec_helper"
RSpec.describe RedisTracking do
- let(:feature) { 'approval_rule' }
let(:user) { create(:user) }
- before do
- skip_feature_flags_yaml_validation
- end
-
controller(ApplicationController) do
include RedisTracking
skip_before_action :authenticate_user!, only: :show
- track_redis_hll_event :index, :show, name: 'g_compliance_approval_rules', feature: :approval_rule, feature_default_enabled: true,
+ track_redis_hll_event :index, :show, name: 'g_compliance_approval_rules',
if: [:custom_condition_one?, :custom_condition_two?]
def index
@@ -49,97 +44,75 @@ RSpec.describe RedisTracking do
expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event)
end
- context 'with feature disabled' do
- it 'does not track the event' do
- stub_feature_flags(feature => false)
-
- expect_no_tracking
-
- get :index
- end
- end
-
- context 'with feature enabled' do
+ context 'when user is logged in' do
before do
- stub_feature_flags(feature => true)
+ sign_in(user)
end
- context 'when user is logged in' do
- before do
- sign_in(user)
- end
-
- it 'tracks the event' do
- expect_tracking
-
- get :index
- end
-
- it 'passes default_enabled flag' do
- expect(controller).to receive(:metric_feature_enabled?).with(feature.to_sym, true)
+ it 'tracks the event' do
+ expect_tracking
- get :index
- end
+ get :index
+ end
- it 'tracks the event if DNT is not enabled' do
- request.headers['DNT'] = '0'
+ it 'tracks the event if DNT is not enabled' do
+ request.headers['DNT'] = '0'
- expect_tracking
+ expect_tracking
- get :index
- end
+ get :index
+ end
- it 'does not track the event if DNT is enabled' do
- request.headers['DNT'] = '1'
+ it 'does not track the event if DNT is enabled' do
+ request.headers['DNT'] = '1'
- expect_no_tracking
+ expect_no_tracking
- get :index
- end
+ get :index
+ end
- it 'does not track the event if the format is not HTML' do
- expect_no_tracking
+ it 'does not track the event if the format is not HTML' do
+ expect_no_tracking
- get :index, format: :json
- end
+ get :index, format: :json
+ end
- it 'does not track the event if a custom condition returns false' do
- expect(controller).to receive(:custom_condition_two?).and_return(false)
+ it 'does not track the event if a custom condition returns false' do
+ expect(controller).to receive(:custom_condition_two?).and_return(false)
- expect_no_tracking
+ expect_no_tracking
- get :index
- end
+ get :index
+ end
- it 'does not track the event for untracked actions' do
- expect_no_tracking
+ it 'does not track the event for untracked actions' do
+ expect_no_tracking
- get :new
- end
+ get :new
end
+ end
- context 'when user is not logged in and there is a visitor_id' do
- let(:visitor_id) { SecureRandom.uuid }
+ context 'when user is not logged in and there is a visitor_id' do
+ let(:visitor_id) { SecureRandom.uuid }
- before do
- routes.draw { get 'show' => 'anonymous#show' }
- end
+ before do
+ routes.draw { get 'show' => 'anonymous#show' }
+ end
- it 'tracks the event' do
- cookies[:visitor_id] = { value: visitor_id, expires: 24.months }
+ it 'tracks the event' do
+ cookies[:visitor_id] = { value: visitor_id, expires: 24.months }
- expect_tracking
+ expect_tracking
- get :show
- end
+ get :show
end
+ end
- context 'when user is not logged in and there is no visitor_id' do
- it 'does not track the event' do
- expect_no_tracking
+ context 'when user is not logged in and there is no visitor_id' do
+ it 'does not track the event' do
+ expect_no_tracking
- get :index
- end
+ get :index
end
end
end
diff --git a/spec/controllers/concerns/spammable_actions_spec.rb b/spec/controllers/concerns/spammable_actions_spec.rb
index 3b5b4d11a9b..25d5398c9da 100644
--- a/spec/controllers/concerns/spammable_actions_spec.rb
+++ b/spec/controllers/concerns/spammable_actions_spec.rb
@@ -6,21 +6,8 @@ RSpec.describe SpammableActions do
controller(ActionController::Base) do
include SpammableActions
- # #create is used to test spammable_params
- # for testing purposes
- def create
- spam_params = spammable_params
-
- # replace the actual request with a string in the JSON response, all we care is that it got set
- spam_params[:request] = 'this is the request' if spam_params[:request]
-
- # just return the params in the response so they can be verified in this fake controller spec.
- # Normally, they are processed further by the controller action
- render json: spam_params.to_json, status: :ok
- end
-
- # #update is used to test recaptcha_check_with_fallback
- # for testing purposes
+ # #update is used here to test #recaptcha_check_with_fallback, but it could be invoked
+ # from #create or any other action which mutates a spammable via a controller.
def update
should_redirect = params[:should_redirect] == 'true'
@@ -35,80 +22,7 @@ RSpec.describe SpammableActions do
end
before do
- # Ordinarily we would not stub a method on the class under test, but :ensure_spam_config_loaded!
- # returns false in the test environment, and is also strong_memoized, so we need to stub it
- allow(controller).to receive(:ensure_spam_config_loaded!) { true }
- end
-
- describe '#spammable_params' do
- subject { post :create, format: :json, params: params }
-
- shared_examples 'expects request param only' do
- it do
- subject
-
- expect(response).to be_successful
- expect(json_response).to eq({ 'request' => 'this is the request' })
- end
- end
-
- shared_examples 'expects all spammable params' do
- it do
- subject
-
- expect(response).to be_successful
- expect(json_response['request']).to eq('this is the request')
- expect(json_response['recaptcha_verified']).to eq(true)
- expect(json_response['spam_log_id']).to eq('1')
- end
- end
-
- let(:recaptcha_response) { nil }
- let(:spam_log_id) { nil }
-
- context 'when recaptcha response is not present' do
- let(:params) do
- {
- spam_log_id: spam_log_id
- }
- end
-
- it_behaves_like 'expects request param only'
- end
-
- context 'when recaptcha response is present' do
- let(:recaptcha_response) { 'abd123' }
- let(:params) do
- {
- 'g-recaptcha-response': recaptcha_response,
- spam_log_id: spam_log_id
- }
- end
-
- context 'when verify_recaptcha returns falsey' do
- before do
- expect(controller).to receive(:verify_recaptcha).with(
- {
- response: recaptcha_response
- }) { false }
- end
-
- it_behaves_like 'expects request param only'
- end
-
- context 'when verify_recaptcha returns truthy' do
- let(:spam_log_id) { 1 }
-
- before do
- expect(controller).to receive(:verify_recaptcha).with(
- {
- response: recaptcha_response
- }) { true }
- end
-
- it_behaves_like 'expects all spammable params'
- end
- end
+ allow(Gitlab::Recaptcha).to receive(:load_configurations!) { true }
end
describe '#recaptcha_check_with_fallback' do
@@ -154,12 +68,9 @@ RSpec.describe SpammableActions do
allow(spammable).to receive(:valid?) { false }
end
- # NOTE: Not adding coverage of details of render_recaptcha?, the plan is to refactor it out
- # of this module anyway as part of adding support for the GraphQL reCAPTCHA flow.
-
- context 'when render_recaptcha? is true' do
+ context 'when spammable.render_recaptcha? is true' do
before do
- expect(controller).to receive(:render_recaptcha?) { true }
+ expect(spammable).to receive(:render_recaptcha?) { true }
end
context 'when format is :html' do
diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb
index e4aea688a69..f10fbf5ef2c 100644
--- a/spec/controllers/graphql_controller_spec.rb
+++ b/spec/controllers/graphql_controller_spec.rb
@@ -66,6 +66,16 @@ RSpec.describe GraphqlController do
expect(assigns(:context)[:is_sessionless_user]).to be false
end
+
+ it 'calls the track api when trackable method' do
+ agent = 'vs-code-gitlab-workflow/3.11.1 VSCode/1.52.1 Node.js/12.14.1 (darwin; x64)'
+ request.env['HTTP_USER_AGENT'] = agent
+
+ expect(Gitlab::UsageDataCounters::VSCodeExtensionActivityUniqueCounter)
+ .to receive(:track_api_request_when_trackable).with(user_agent: agent, user: user)
+
+ post :execute
+ end
end
context 'when user uses an API token' do
@@ -83,6 +93,16 @@ RSpec.describe GraphqlController do
expect(assigns(:context)[:is_sessionless_user]).to be true
end
+
+ it 'calls the track api when trackable method' do
+ agent = 'vs-code-gitlab-workflow/3.11.1 VSCode/1.52.1 Node.js/12.14.1 (darwin; x64)'
+ request.env['HTTP_USER_AGENT'] = agent
+
+ expect(Gitlab::UsageDataCounters::VSCodeExtensionActivityUniqueCounter)
+ .to receive(:track_api_request_when_trackable).with(user_agent: agent, user: user)
+
+ subject
+ end
end
context 'when user is not logged in' do
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index 4d78419e8eb..ff7a7f55863 100644
--- a/spec/controllers/groups/group_members_controller_spec.rb
+++ b/spec/controllers/groups/group_members_controller_spec.rb
@@ -225,6 +225,18 @@ RSpec.describe Groups::GroupMembersController do
expect(requester.reload.expires_at).not_to eq(expires_at.to_date)
end
+
+ it 'returns error status' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ end
+
+ it 'returns error message' do
+ subject
+
+ expect(json_response).to eq({ 'message' => 'Expires at cannot be a date in the past' })
+ end
end
context 'when set to a date in the future' do
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index 939c36a98b2..9e5f68820d9 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -306,66 +306,6 @@ RSpec.describe GroupsController, factory_default: :keep do
end
end
end
-
- describe 'tracking group creation for onboarding issues experiment' do
- before do
- sign_in(user)
- end
-
- subject(:create_namespace) { post :create, params: { group: { name: 'new_group', path: 'new_group' } } }
-
- context 'experiment disabled' do
- before do
- stub_experiment(onboarding_issues: false)
- end
-
- it 'does not track anything', :snowplow do
- create_namespace
-
- expect_no_snowplow_event
- end
- end
-
- context 'experiment enabled' do
- before do
- stub_experiment(onboarding_issues: true)
- end
-
- context 'and the user is part of the control group' do
- before do
- stub_experiment_for_subject(onboarding_issues: false)
- end
-
- it 'tracks the event with the "created_namespace" action with the "control_group" property', :snowplow do
- create_namespace
-
- expect_snowplow_event(
- category: 'Growth::Conversion::Experiment::OnboardingIssues',
- action: 'created_namespace',
- label: anything,
- property: 'control_group'
- )
- end
- end
-
- context 'and the user is part of the experimental group' do
- before do
- stub_experiment_for_subject(onboarding_issues: true)
- end
-
- it 'tracks the event with the "created_namespace" action with the "experimental_group" property', :snowplow do
- create_namespace
-
- expect_snowplow_event(
- category: 'Growth::Conversion::Experiment::OnboardingIssues',
- action: 'created_namespace',
- label: anything,
- property: 'experimental_group'
- )
- end
- end
- end
- end
end
describe 'GET #index' do
diff --git a/spec/controllers/help_controller_spec.rb b/spec/controllers/help_controller_spec.rb
index 6927df3b1c7..629d9b50d73 100644
--- a/spec/controllers/help_controller_spec.rb
+++ b/spec/controllers/help_controller_spec.rb
@@ -7,6 +7,43 @@ RSpec.describe HelpController do
let(:user) { create(:user) }
+ shared_examples 'documentation pages local render' do
+ it 'renders HTML' do
+ aggregate_failures do
+ is_expected.to render_template('show.html.haml')
+ expect(response.media_type).to eq 'text/html'
+ end
+ end
+ end
+
+ shared_examples 'documentation pages redirect' do |documentation_base_url|
+ let(:gitlab_version) { '13.4.0-ee' }
+
+ before do
+ stub_version(gitlab_version, 'ignored_revision_value')
+ end
+
+ it 'redirects user to custom documentation url with a specified version' do
+ is_expected.to redirect_to("#{documentation_base_url}/13.4/ee/#{path}.html")
+ end
+
+ context 'when it is a pre-release' do
+ let(:gitlab_version) { '13.4.0-pre' }
+
+ it 'redirects user to custom documentation url without a version' do
+ is_expected.to redirect_to("#{documentation_base_url}/ee/#{path}.html")
+ end
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(help_page_documentation_redirect: false)
+ end
+
+ it_behaves_like 'documentation pages local render'
+ end
+ end
+
before do
sign_in(user)
end
@@ -99,69 +136,70 @@ RSpec.describe HelpController do
describe 'GET #show' do
context 'for Markdown formats' do
+ subject { get :show, params: { path: path }, format: :md }
+
+ let(:path) { 'ssh/README' }
+
context 'when requested file exists' do
before do
expect_file_read(File.join(Rails.root, 'doc/ssh/README.md'), content: fixture_file('blockquote_fence_after.md'))
- get :show, params: { path: 'ssh/README' }, format: :md
+ subject
end
it 'assigns to @markdown' do
expect(assigns[:markdown]).not_to be_empty
end
- it 'renders HTML' do
- aggregate_failures do
- expect(response).to render_template('show.html.haml')
- expect(response.media_type).to eq 'text/html'
- end
- end
+ it_behaves_like 'documentation pages local render'
end
- context 'when a custom help_page_documentation_url is set' do
+ context 'when a custom help_page_documentation_url is set in database' do
before do
- stub_application_setting(help_page_documentation_base_url: documentation_base_url)
- stub_version(gitlab_version, 'deadbeaf')
+ stub_application_setting(help_page_documentation_base_url: 'https://in-db.gitlab.com')
end
- subject { get :show, params: { path: path }, format: 'html' }
+ it_behaves_like 'documentation pages redirect', 'https://in-db.gitlab.com'
+ end
- let(:gitlab_version) { '13.4.0-ee' }
- let(:documentation_base_url) { 'https://docs.gitlab.com' }
- let(:path) { 'ssh/README' }
+ context 'when a custom help_page_documentation_url is set in configuration file' do
+ let(:host) { 'https://in-yaml.gitlab.com' }
+ let(:docs_enabled) { true }
- it 'redirects user to custom documentation url with a specified version' do
- is_expected.to redirect_to("#{documentation_base_url}/13.4/ee/#{path}.html")
+ before do
+ allow(Settings).to receive(:gitlab_docs) { double(enabled: docs_enabled, host: host) }
end
- context 'when documentation url ends with a slash' do
- let(:documentation_base_url) { 'https://docs.gitlab.com/' }
+ it_behaves_like 'documentation pages redirect', 'https://in-yaml.gitlab.com'
- it 'redirects user to custom documentation url without slash duplicates' do
- is_expected.to redirect_to("https://docs.gitlab.com/13.4/ee/#{path}.html")
- end
+ context 'when gitlab_docs is disabled' do
+ let(:docs_enabled) { false }
+
+ it_behaves_like 'documentation pages local render'
end
- context 'when it is a pre-release' do
- let(:gitlab_version) { '13.4.0-pre' }
+ context 'when host is missing' do
+ let(:host) { nil }
- it 'redirects user to custom documentation url without a version' do
- is_expected.to redirect_to("#{documentation_base_url}/ee/#{path}.html")
- end
+ it_behaves_like 'documentation pages local render'
end
+ end
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(help_page_documentation_redirect: false)
- end
+ context 'when help_page_documentation_url is set in both db and configuration file' do
+ before do
+ stub_application_setting(help_page_documentation_base_url: 'https://in-db.gitlab.com')
+ allow(Settings).to receive(:gitlab_docs) { double(enabled: true, host: 'https://in-yaml.gitlab.com') }
+ end
- it 'renders HTML' do
- aggregate_failures do
- is_expected.to render_template('show.html.haml')
- expect(response.media_type).to eq 'text/html'
- end
- end
+ it_behaves_like 'documentation pages redirect', 'https://in-yaml.gitlab.com'
+ end
+
+ context 'when help_page_documentation_url has a trailing slash' do
+ before do
+ allow(Settings).to receive(:gitlab_docs) { double(enabled: true, host: 'https://in-yaml.gitlab.com/') }
end
+
+ it_behaves_like 'documentation pages redirect', 'https://in-yaml.gitlab.com'
end
context 'when requested file is missing' do
diff --git a/spec/controllers/import/bulk_imports_controller_spec.rb b/spec/controllers/import/bulk_imports_controller_spec.rb
index d1c138617bb..08a54f112bb 100644
--- a/spec/controllers/import/bulk_imports_controller_spec.rb
+++ b/spec/controllers/import/bulk_imports_controller_spec.rb
@@ -59,7 +59,14 @@ RSpec.describe Import::BulkImportsController do
parsed_response: [
{ 'id' => 1, 'full_name' => 'group1', 'full_path' => 'full/path/group1', 'web_url' => 'http://demo.host/full/path/group1' },
{ 'id' => 2, 'full_name' => 'group2', 'full_path' => 'full/path/group2', 'web_url' => 'http://demo.host/full/path/group1' }
- ]
+ ],
+ headers: {
+ 'x-next-page' => '2',
+ 'x-page' => '1',
+ 'x-per-page' => '20',
+ 'x-total' => '37',
+ 'x-total-pages' => '2'
+ }
)
end
@@ -81,6 +88,17 @@ RSpec.describe Import::BulkImportsController do
expect(json_response).to eq({ importable_data: client_response.parsed_response }.as_json)
end
+ it 'forwards pagination headers' do
+ get :status, format: :json
+
+ expect(response.headers['x-per-page']).to eq client_response.headers['x-per-page']
+ expect(response.headers['x-page']).to eq client_response.headers['x-page']
+ expect(response.headers['x-next-page']).to eq client_response.headers['x-next-page']
+ expect(response.headers['x-prev-page']).to eq client_response.headers['x-prev-page']
+ expect(response.headers['x-total']).to eq client_response.headers['x-total']
+ expect(response.headers['x-total-pages']).to eq client_response.headers['x-total-pages']
+ end
+
context 'when filtering' do
it 'returns filtered result' do
filter = 'test'
@@ -167,6 +185,7 @@ RSpec.describe Import::BulkImportsController do
describe 'POST create' do
let(:instance_url) { "http://fake-intance" }
+ let(:bulk_import) { create(:bulk_import) }
let(:pat) { "fake-pat" }
before do
@@ -183,12 +202,13 @@ RSpec.describe Import::BulkImportsController do
expect_next_instance_of(
BulkImportService, user, bulk_import_params, { url: instance_url, access_token: pat }) do |service|
- expect(service).to receive(:execute)
+ allow(service).to receive(:execute).and_return(bulk_import)
end
post :create, params: { bulk_import: bulk_import_params }
expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to eq({ id: bulk_import.id }.to_json)
end
end
end
diff --git a/spec/controllers/invites_controller_spec.rb b/spec/controllers/invites_controller_spec.rb
index e863f5ef2fc..a8d38d12f23 100644
--- a/spec/controllers/invites_controller_spec.rb
+++ b/spec/controllers/invites_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe InvitesController, :snowplow do
+RSpec.describe InvitesController do
let_it_be(:user) { create(:user) }
let(:member) { create(:project_member, :invited, invite_email: user.email) }
let(:raw_invite_token) { member.raw_invite_token }
@@ -51,6 +51,28 @@ RSpec.describe InvitesController, :snowplow do
end
it_behaves_like 'invalid token'
+
+ context 'when invite comes from the initial email invite' do
+ let(:params) { { id: raw_invite_token, invite_type: Members::InviteEmailExperiment::INVITE_TYPE } }
+
+ it 'tracks via experiment', :aggregate_failures do
+ experiment = double(track: true)
+ allow(controller).to receive(:experiment).and_return(experiment)
+
+ request
+
+ expect(experiment).to have_received(:track).with(:opened)
+ expect(experiment).to have_received(:track).with(:accepted)
+ end
+ end
+
+ context 'when invite does not come from initial email invite' do
+ it 'does not track via experiment' do
+ expect(controller).not_to receive(:experiment)
+
+ request
+ end
+ end
end
context 'when not logged in' do
@@ -82,6 +104,25 @@ RSpec.describe InvitesController, :snowplow do
subject(:request) { post :accept, params: params }
it_behaves_like 'invalid token'
+
+ context 'when invite comes from the initial email invite' do
+ it 'tracks via experiment' do
+ experiment = double(track: true)
+ allow(controller).to receive(:experiment).and_return(experiment)
+
+ post :accept, params: params, session: { invite_type: Members::InviteEmailExperiment::INVITE_TYPE }
+
+ expect(experiment).to have_received(:track).with(:accepted)
+ end
+ end
+
+ context 'when invite does not come from initial email invite' do
+ it 'does not track via experiment' do
+ expect(controller).not_to receive(:experiment)
+
+ request
+ end
+ end
end
describe 'POST #decline for link in UI' do
diff --git a/spec/controllers/profiles/notifications_controller_spec.rb b/spec/controllers/profiles/notifications_controller_spec.rb
index 90df7cc0991..03749366703 100644
--- a/spec/controllers/profiles/notifications_controller_spec.rb
+++ b/spec/controllers/profiles/notifications_controller_spec.rb
@@ -119,10 +119,11 @@ RSpec.describe Profiles::NotificationsController do
it 'updates only permitted attributes' do
sign_in(user)
- put :update, params: { user: { notification_email: 'new@example.com', notified_of_own_activity: true, admin: true } }
+ put :update, params: { user: { notification_email: 'new@example.com', email_opted_in: true, notified_of_own_activity: true, admin: true } }
user.reload
expect(user.notification_email).to eq('new@example.com')
+ expect(user.email_opted_in).to eq(true)
expect(user.notified_of_own_activity).to eq(true)
expect(user.admin).to eq(false)
expect(controller).to set_flash[:notice].to('Notification settings saved')
diff --git a/spec/controllers/profiles/preferences_controller_spec.rb b/spec/controllers/profiles/preferences_controller_spec.rb
index 4a68475c37f..b7870a63f9d 100644
--- a/spec/controllers/profiles/preferences_controller_spec.rb
+++ b/spec/controllers/profiles/preferences_controller_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe Profiles::PreferencesController do
end
describe 'PATCH update' do
- def go(params: {}, format: :js)
+ def go(params: {}, format: :json)
params.reverse_merge!(
color_scheme_id: '1',
dashboard: 'stars',
@@ -35,9 +35,12 @@ RSpec.describe Profiles::PreferencesController do
end
context 'on successful update' do
- it 'sets the flash' do
+ it 'responds with success' do
go
- expect(flash[:notice]).to eq _('Preferences saved.')
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.parsed_body['message']).to eq _('Preferences saved.')
+ expect(response.parsed_body['type']).to eq('notice')
end
it "changes the user's preferences" do
@@ -59,36 +62,26 @@ RSpec.describe Profiles::PreferencesController do
end
context 'on failed update' do
- it 'sets the flash' do
+ it 'responds with error' do
expect(user).to receive(:save).and_return(false)
go
- expect(flash[:alert]).to eq(_('Failed to save preferences.'))
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(response.parsed_body['message']).to eq _('Failed to save preferences.')
+ expect(response.parsed_body['type']).to eq('alert')
end
end
context 'on invalid dashboard setting' do
- it 'sets the flash' do
+ it 'responds with error' do
prefs = { dashboard: 'invalid' }
go params: prefs
- expect(flash[:alert]).to match(/\AFailed to save preferences \(.+\)\.\z/)
- end
- end
-
- context 'as js' do
- it 'renders' do
- go
- expect(response).to render_template :update
- end
- end
-
- context 'as html' do
- it 'redirects' do
- go format: :html
- expect(response).to redirect_to(profile_preferences_path)
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(response.parsed_body['message']).to match(/\AFailed to save preferences \(.+\)\.\z/)
+ expect(response.parsed_body['type']).to eq('alert')
end
end
end
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index 16be7394174..68551ce4858 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -424,7 +424,7 @@ RSpec.describe Projects::BlobController do
end
end
- it_behaves_like 'tracking unique hll events', :track_editor_edit_actions do
+ it_behaves_like 'tracking unique hll events' do
subject(:request) { put :update, params: default_params }
let(:target_id) { 'g_edit_by_sfe' }
@@ -540,7 +540,7 @@ RSpec.describe Projects::BlobController do
sign_in(user)
end
- it_behaves_like 'tracking unique hll events', :track_editor_edit_actions do
+ it_behaves_like 'tracking unique hll events' do
subject(:request) { post :create, params: default_params }
let(:target_id) { 'g_edit_by_sfe' }
diff --git a/spec/controllers/projects/ci/daily_build_group_report_results_controller_spec.rb b/spec/controllers/projects/ci/daily_build_group_report_results_controller_spec.rb
index 594c24bb7e3..81318b49cd9 100644
--- a/spec/controllers/projects/ci/daily_build_group_report_results_controller_spec.rb
+++ b/spec/controllers/projects/ci/daily_build_group_report_results_controller_spec.rb
@@ -11,6 +11,7 @@ RSpec.describe Projects::Ci::DailyBuildGroupReportResultsController do
let(:end_date) { '2020-03-09' }
let(:allowed_to_read) { true }
let(:user) { create(:user) }
+ let(:feature_enabled?) { true }
before do
create_daily_coverage('rspec', 79.0, '2020-03-09')
@@ -24,6 +25,8 @@ RSpec.describe Projects::Ci::DailyBuildGroupReportResultsController do
allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?).with(user, :read_build_report_results, project).and_return(allowed_to_read)
+ stub_feature_flags(coverage_data_new_finder: feature_enabled?)
+
get :index, params: {
namespace_id: project.namespace,
project_id: project,
@@ -55,9 +58,7 @@ RSpec.describe Projects::Ci::DailyBuildGroupReportResultsController do
end
end
- context 'when format is CSV' do
- let(:format) { :csv }
-
+ shared_examples 'CSV results' do
it 'serves the results in CSV' do
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers['Content-Type']).to eq('text/csv; charset=utf-8')
@@ -88,9 +89,7 @@ RSpec.describe Projects::Ci::DailyBuildGroupReportResultsController do
it_behaves_like 'ensuring policy'
end
- context 'when format is JSON' do
- let(:format) { :json }
-
+ shared_examples 'JSON results' do
it 'serves the results in JSON' do
expect(response).to have_gitlab_http_status(:ok)
@@ -137,6 +136,38 @@ RSpec.describe Projects::Ci::DailyBuildGroupReportResultsController do
it_behaves_like 'validating param_type'
it_behaves_like 'ensuring policy'
end
+
+ context 'when format is JSON' do
+ let(:format) { :json }
+
+ context 'when coverage_data_new_finder flag is enabled' do
+ let(:feature_enabled?) { true }
+
+ it_behaves_like 'JSON results'
+ end
+
+ context 'when coverage_data_new_finder flag is disabled' do
+ let(:feature_enabled?) { false }
+
+ it_behaves_like 'JSON results'
+ end
+ end
+
+ context 'when format is CSV' do
+ let(:format) { :csv }
+
+ context 'when coverage_data_new_finder flag is enabled' do
+ let(:feature_enabled?) { true }
+
+ it_behaves_like 'CSV results'
+ end
+
+ context 'when coverage_data_new_finder flag is disabled' do
+ let(:feature_enabled?) { false }
+
+ it_behaves_like 'CSV results'
+ end
+ end
end
def create_daily_coverage(group_name, coverage, date)
diff --git a/spec/controllers/projects/discussions_controller_spec.rb b/spec/controllers/projects/discussions_controller_spec.rb
index f9d16e761cb..8a793e29bfa 100644
--- a/spec/controllers/projects/discussions_controller_spec.rb
+++ b/spec/controllers/projects/discussions_controller_spec.rb
@@ -186,6 +186,13 @@ RSpec.describe Projects::DiscussionsController do
expect(Note.find(note.id).discussion.resolved?).to be false
end
+ it "tracks thread unresolve usage data" do
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to receive(:track_unresolve_thread_action).with(user: user)
+
+ delete :unresolve, params: request_params
+ end
+
it "returns status 200" do
delete :unresolve, params: request_params
diff --git a/spec/controllers/projects/forks_controller_spec.rb b/spec/controllers/projects/forks_controller_spec.rb
index e8b30294cdd..7da3d403b53 100644
--- a/spec/controllers/projects/forks_controller_spec.rb
+++ b/spec/controllers/projects/forks_controller_spec.rb
@@ -209,6 +209,13 @@ RSpec.describe Projects::ForksController do
}
end
+ let(:created_project) do
+ Namespace
+ .find_by_id(params[:namespace_key])
+ .projects
+ .find_by_path(params.fetch(:path, project.path))
+ end
+
subject do
post :create, params: params
end
@@ -260,6 +267,21 @@ RSpec.describe Projects::ForksController do
expect(response).to redirect_to(namespace_project_import_path(user.namespace, project, continue: continue_params))
end
end
+
+ context 'custom attributes set' do
+ let(:params) { super().merge(path: 'something_custom', name: 'Something Custom', description: 'Something Custom', visibility: 'private') }
+
+ it 'creates a project with custom values' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:found)
+ expect(response).to redirect_to(namespace_project_import_path(user.namespace, params[:path]))
+ expect(created_project.path).to eq(params[:path])
+ expect(created_project.name).to eq(params[:name])
+ expect(created_project.description).to eq(params[:description])
+ expect(created_project.visibility).to eq(params[:visibility])
+ end
+ end
end
context 'when user is not signed in' do
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index d3bdf1baaae..81ffd2c4512 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -63,53 +63,20 @@ RSpec.describe Projects::IssuesController do
end
end
- describe 'the null hypothesis experiment', :snowplow do
- it 'defines the expected before actions' do
- expect(controller).to use_before_action(:run_null_hypothesis_experiment)
- end
-
- context 'when rolled out to 100%' do
- it 'assigns the candidate experience and tracks the event' do
- get :index, params: { namespace_id: project.namespace, project_id: project }
-
- expect_snowplow_event(
- category: 'null_hypothesis',
- action: 'index',
- context: [{
- schema: 'iglu:com.gitlab/gitlab_experiment/jsonschema/0-3-0',
- data: { variant: 'candidate', experiment: 'null_hypothesis', key: anything }
- }]
- )
- end
+ describe 'the null hypothesis experiment', :experiment do
+ before do
+ stub_experiments(null_hypothesis: :candidate)
end
- context 'when not rolled out' do
- before do
- stub_feature_flags(null_hypothesis: false)
- end
-
- it 'assigns the control experience and tracks the event' do
- get :index, params: { namespace_id: project.namespace, project_id: project }
-
- expect_snowplow_event(
- category: 'null_hypothesis',
- action: 'index',
- context: [{
- schema: 'iglu:com.gitlab/gitlab_experiment/jsonschema/0-3-0',
- data: { variant: 'control', experiment: 'null_hypothesis', key: anything }
- }]
- )
- end
+ it 'defines the expected before actions' do
+ expect(controller).to use_before_action(:run_null_hypothesis_experiment)
end
- context 'when gitlab_experiments is disabled' do
- it 'does not run the experiment at all' do
- stub_feature_flags(gitlab_experiments: false)
+ it 'assigns the candidate experience and tracks the event' do
+ expect(experiment(:null_hypothesis)).to track('index').on_any_instance.for(:candidate)
+ .with_context(project: project)
- expect(controller).not_to receive(:run_null_hypothesis_experiment)
-
- get :index, params: { namespace_id: project.namespace, project_id: project }
- end
+ get :index, params: { namespace_id: project.namespace, project_id: project }
end
end
end
@@ -1314,11 +1281,13 @@ 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': true } )
+ post_new_issue({}, { spam_log_id: last_spam_log.id, 'g-recaptcha-response': 'abc123' } )
end
before do
- expect(controller).to receive_messages(verify_recaptcha: true)
+ expect_next_instance_of(Captcha::CaptchaVerificationService) do |instance|
+ expect(instance).to receive(:execute) { true }
+ end
end
it 'accepts an issue after reCAPTCHA is verified' do
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index 430808e1c63..80e1268cb01 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -15,54 +15,6 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
describe 'GET index' do
- describe 'pushing tracking_data to Gon' do
- before do
- stub_experiment(jobs_empty_state: experiment_active)
- stub_experiment_for_subject(jobs_empty_state: in_experiment_group)
-
- get_index
- end
-
- context 'when experiment not active' do
- let(:experiment_active) { false }
- let(:in_experiment_group) { false }
-
- it 'does not push tracking_data to Gon' do
- expect(Gon.tracking_data).to be_nil
- end
- end
-
- context 'when experiment active and user in control group' do
- let(:experiment_active) { true }
- let(:in_experiment_group) { false }
-
- it 'pushes tracking_data to Gon' do
- expect(Gon.tracking_data).to match(
- {
- category: 'Growth::Activation::Experiment::JobsEmptyState',
- action: 'click_button',
- label: anything,
- property: 'control_group'
- }
- )
- end
- end
-
- context 'when experiment active and user in experimental group' do
- let(:experiment_active) { true }
- let(:in_experiment_group) { true }
-
- it 'pushes tracking_data to gon' do
- expect(Gon.tracking_data).to match(
- category: 'Growth::Activation::Experiment::JobsEmptyState',
- action: 'click_button',
- label: anything,
- property: 'experimental_group'
- )
- end
- end
- end
-
context 'when scope is pending' do
before do
create(:ci_build, :pending, pipeline: pipeline)
diff --git a/spec/controllers/projects/learn_gitlab_controller_spec.rb b/spec/controllers/projects/learn_gitlab_controller_spec.rb
new file mode 100644
index 00000000000..f633f7aa246
--- /dev/null
+++ b/spec/controllers/projects/learn_gitlab_controller_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::LearnGitlabController do
+ describe 'GET #index' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, namespace: user.namespace) }
+
+ let(:learn_gitlab_experiment_enabled) { true }
+ let(:params) { { namespace_id: project.namespace.to_param, project_id: project } }
+
+ subject { get :index, params: params }
+
+ before do
+ allow(controller.helpers).to receive(:learn_gitlab_experiment_enabled?).and_return(learn_gitlab_experiment_enabled)
+ end
+
+ context 'unauthenticated user' do
+ it { is_expected.to have_gitlab_http_status(:redirect) }
+ end
+
+ context 'authenticated user' do
+ before do
+ sign_in(user)
+ end
+
+ it { is_expected.to render_template(:index) }
+
+ it 'pushes experiment to frontend' do
+ expect(controller).to receive(:push_frontend_experiment).with(:learn_gitlab_a, subject: user)
+ expect(controller).to receive(:push_frontend_experiment).with(:learn_gitlab_b, subject: user)
+
+ subject
+ end
+
+ context 'learn_gitlab experiment not enabled' do
+ let(:learn_gitlab_experiment_enabled) { false }
+
+ it { is_expected.to have_gitlab_http_status(:not_found) }
+ 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 f54a07de853..50f8942d9d5 100644
--- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
@@ -226,11 +226,7 @@ RSpec.describe Projects::MergeRequests::DiffsController do
let(:diffable_merge_ref) { true }
it 'compares diffs with the head' do
- MergeRequests::MergeToRefService.new(project, merge_request.author).execute(merge_request)
-
- expect(CompareService).to receive(:new).with(
- project, merge_request.merge_ref_head.sha
- ).and_call_original
+ create(:merge_request_diff, :merge_head, merge_request: merge_request)
go(diff_head: true)
@@ -242,8 +238,6 @@ RSpec.describe Projects::MergeRequests::DiffsController do
let(:diffable_merge_ref) { false }
it 'compares diffs with the base' do
- expect(CompareService).not_to receive(:new)
-
go(diff_head: true)
expect(response).to have_gitlab_http_status(:ok)
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index cf8b4c564c4..9b37c46fd86 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -1118,6 +1118,108 @@ RSpec.describe Projects::MergeRequestsController do
end
end
+ describe 'GET codequality_mr_diff_reports' do
+ let_it_be(:merge_request) do
+ create(:merge_request,
+ :with_merge_request_pipeline,
+ target_project: project,
+ source_project: project)
+ end
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ :success,
+ project: merge_request.source_project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha)
+ end
+
+ before do
+ allow_any_instance_of(MergeRequest)
+ .to receive(:find_codequality_mr_diff_reports)
+ .and_return(report)
+
+ allow_any_instance_of(MergeRequest)
+ .to receive(:actual_head_pipeline)
+ .and_return(pipeline)
+ end
+
+ subject(:get_codequality_mr_diff_reports) do
+ get :codequality_mr_diff_reports, params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: merge_request.iid
+ },
+ format: :json
+ end
+
+ context 'permissions on a public project with private CI/CD' do
+ let(:project) { create :project, :repository, :public, :builds_private }
+ let(:report) { { status: :parsed, data: { 'files' => {} } } }
+
+ context 'while signed out' do
+ before do
+ sign_out(user)
+ end
+
+ it 'responds with a 404' do
+ get_codequality_mr_diff_reports
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(response.body).to be_blank
+ end
+ end
+
+ context 'while signed in as an unrelated user' do
+ before do
+ sign_in(create(:user))
+ end
+
+ it 'responds with a 404' do
+ get_codequality_mr_diff_reports
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(response.body).to be_blank
+ end
+ end
+ end
+
+ context 'when pipeline has jobs with codequality mr diff report' do
+ before do
+ allow_any_instance_of(MergeRequest)
+ .to receive(:has_codequality_mr_diff_report?)
+ .and_return(true)
+ end
+
+ context 'when processing codequality mr diff report is in progress' do
+ let(:report) { { status: :parsing } }
+
+ it 'sends polling interval' do
+ expect(Gitlab::PollingInterval).to receive(:set_header)
+
+ get_codequality_mr_diff_reports
+ end
+
+ it 'returns 204 HTTP status' do
+ get_codequality_mr_diff_reports
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+ end
+
+ context 'when processing codequality mr diff report is completed' do
+ let(:report) { { status: :parsed, data: { 'files' => {} } } }
+
+ it 'returns codequality mr diff report' do
+ get_codequality_mr_diff_reports
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq({ 'files' => {} })
+ end
+ end
+ end
+ end
+
describe 'GET terraform_reports' do
let_it_be(:merge_request) do
create(:merge_request,
@@ -1269,7 +1371,6 @@ RSpec.describe Projects::MergeRequestsController do
describe 'GET test_reports' do
let_it_be(:merge_request) do
create(:merge_request,
- :with_diffs,
:with_merge_request_pipeline,
target_project: project,
source_project: project
@@ -1380,7 +1481,6 @@ RSpec.describe Projects::MergeRequestsController do
describe 'GET accessibility_reports' do
let_it_be(:merge_request) do
create(:merge_request,
- :with_diffs,
:with_merge_request_pipeline,
target_project: project,
source_project: project
@@ -1501,7 +1601,6 @@ RSpec.describe Projects::MergeRequestsController do
describe 'GET codequality_reports' do
let_it_be(:merge_request) do
create(:merge_request,
- :with_diffs,
:with_merge_request_pipeline,
target_project: project,
source_project: project
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index e96113c0133..edebaf294c4 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -150,7 +150,7 @@ RSpec.describe Projects::NotesController do
end
it 'returns an empty page of notes' do
- expect(Gitlab::EtagCaching::Middleware).not_to receive(:skip!)
+ expect(Gitlab::EtagCaching::Middleware).to receive(:skip!)
request.headers['X-Last-Fetched-At'] = microseconds(Time.zone.now)
@@ -169,6 +169,8 @@ RSpec.describe Projects::NotesController do
end
it 'returns all notes' do
+ expect(Gitlab::EtagCaching::Middleware).to receive(:skip!)
+
get :index, params: request_params
expect(json_response['notes'].count).to eq((page_1 + page_2 + page_3).size + 1)
@@ -313,7 +315,7 @@ RSpec.describe Projects::NotesController do
let(:note_text) { 'some note' }
let(:request_params) do
{
- note: { note: note_text, noteable_id: merge_request.id, noteable_type: 'MergeRequest' },
+ note: { note: note_text, noteable_id: merge_request.id, noteable_type: 'MergeRequest' }.merge(extra_note_params),
namespace_id: project.namespace,
project_id: project,
merge_request_diff_head_sha: 'sha',
@@ -323,6 +325,7 @@ RSpec.describe Projects::NotesController do
end
let(:extra_request_params) { {} }
+ let(:extra_note_params) { {} }
let(:project_visibility) { Gitlab::VisibilityLevel::PUBLIC }
let(:merge_requests_access_level) { ProjectFeature::ENABLED }
@@ -421,6 +424,41 @@ RSpec.describe Projects::NotesController do
end
end
+ context 'when creating a confidential note' do
+ let(:extra_request_params) { { format: :json } }
+
+ context 'when `confidential` parameter is not provided' do
+ it 'sets `confidential` to `false` in JSON response' do
+ create!
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['confidential']).to be false
+ end
+ end
+
+ context 'when `confidential` parameter is `false`' do
+ let(:extra_note_params) { { confidential: false } }
+
+ it 'sets `confidential` to `false` in JSON response' do
+ create!
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['confidential']).to be false
+ end
+ end
+
+ context 'when `confidential` parameter is `true`' do
+ let(:extra_note_params) { { confidential: true } }
+
+ it 'sets `confidential` to `true` in JSON response' do
+ create!
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['confidential']).to be true
+ end
+ end
+ end
+
context 'when creating a note with quick actions' do
context 'with commands that return changes' do
let(:note_text) { "/award :thumbsup:\n/estimate 1d\n/spend 3h" }
@@ -725,6 +763,51 @@ RSpec.describe Projects::NotesController do
end
end
end
+
+ context 'when the endpoint receives requests above the limit' do
+ before do
+ stub_application_setting(notes_create_limit: 3)
+ end
+
+ it 'prevents from creating more notes', :request_store do
+ 3.times { create! }
+
+ expect { create! }
+ .to change { Gitlab::GitalyClient.get_request_count }.by(0)
+
+ create!
+ expect(response.body).to eq(_('This endpoint has been requested too many times. Try again later.'))
+ expect(response).to have_gitlab_http_status(:too_many_requests)
+ end
+
+ it 'logs the event in auth.log' do
+ attributes = {
+ message: 'Application_Rate_Limiter_Request',
+ env: :notes_create_request_limit,
+ remote_ip: '0.0.0.0',
+ request_method: 'POST',
+ path: "/#{project.full_path}/notes",
+ user_id: user.id,
+ username: user.username
+ }
+
+ expect(Gitlab::AuthLogger).to receive(:error).with(attributes).once
+
+ project.add_developer(user)
+ sign_in(user)
+
+ 4.times { create! }
+ end
+
+ it 'allows user in allow-list to create notes, even if the case is different' do
+ user.update_attribute(:username, user.username.titleize)
+ stub_application_setting(notes_create_limit_allowlist: ["#{user.username.downcase}"])
+ 3.times { create! }
+
+ create!
+ expect(response).to have_gitlab_http_status(:found)
+ end
+ end
end
describe 'PUT update' do
diff --git a/spec/controllers/projects/pipelines/tests_controller_spec.rb b/spec/controllers/projects/pipelines/tests_controller_spec.rb
index 61118487e20..e6ff3a487ac 100644
--- a/spec/controllers/projects/pipelines/tests_controller_spec.rb
+++ b/spec/controllers/projects/pipelines/tests_controller_spec.rb
@@ -34,20 +34,38 @@ RSpec.describe Projects::Pipelines::TestsController do
end
describe 'GET #show.json' do
- context 'when pipeline has build report results' do
- let(:pipeline) { create(:ci_pipeline, :with_report_results, project: project) }
+ context 'when pipeline has builds with test reports' do
+ let(:main_pipeline) { create(:ci_pipeline, :with_test_reports_with_three_failures, project: project) }
+ let(:pipeline) { create(:ci_pipeline, :with_test_reports_with_three_failures, project: project, ref: 'new-feature') }
let(:suite_name) { 'test' }
let(:build_ids) { pipeline.latest_builds.pluck(:id) }
+ before do
+ build = main_pipeline.builds.last
+ build.update_column(:finished_at, 1.day.ago) # Just to be sure we are included in the report window
+
+ # The JUnit fixture for the given build has 3 failures.
+ # This service will create 1 test case failure record for each.
+ Ci::TestFailureHistoryService.new(main_pipeline).execute
+ end
+
it 'renders test suite data' do
get_tests_show_json(build_ids)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['name']).to eq('test')
+
+ # Each test failure in this pipeline has a matching failure in the default branch
+ recent_failures = json_response['test_cases'].map { |tc| tc['recent_failures'] }
+ expect(recent_failures).to eq([
+ { 'count' => 1, 'base_branch' => 'master' },
+ { 'count' => 1, 'base_branch' => 'master' },
+ { 'count' => 1, 'base_branch' => 'master' }
+ ])
end
end
- context 'when pipeline does not have build report results' do
+ context 'when pipeline has no builds that matches the given build_ids' do
let(:pipeline) { create(:ci_empty_pipeline) }
let(:suite_name) { 'test' }
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index be4a1504fc9..e1405660ccb 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -272,72 +272,6 @@ RSpec.describe Projects::PipelinesController do
end
end
- describe 'GET #index' do
- subject(:request) { get :index, params: { namespace_id: project.namespace, project_id: project } }
-
- context 'experiment not active' do
- it 'does not push tracking_data to gon' do
- request
-
- expect(Gon.tracking_data).to be_nil
- end
-
- it 'does not record experiment_user' do
- expect { request }.not_to change(ExperimentUser, :count)
- end
- end
-
- context 'when experiment active' do
- before do
- stub_experiment(pipelines_empty_state: true)
- stub_experiment_for_subject(pipelines_empty_state: true)
- end
-
- it 'pushes tracking_data to Gon' do
- request
-
- expect(Gon.experiments["pipelinesEmptyState"]).to eq(true)
- expect(Gon.tracking_data).to match(
- {
- category: 'Growth::Activation::Experiment::PipelinesEmptyState',
- action: 'view',
- label: anything,
- property: 'experimental_group',
- value: anything
- }
- )
- end
-
- context 'no pipelines created an no CI set up' do
- before do
- stub_application_setting(auto_devops_enabled: false)
- end
-
- it 'records experiment_user' do
- expect { request }.to change(ExperimentUser, :count).by(1)
- end
- end
-
- context 'CI set up' do
- it 'does not record experiment_user' do
- expect { request }.not_to change(ExperimentUser, :count)
- end
- end
-
- context 'pipelines created' do
- let!(:pipeline) { create(:ci_pipeline, project: project) }
-
- before do
- stub_application_setting(auto_devops_enabled: false)
- end
-
- it 'does not record experiment_user' do
- expect { request }.not_to change(ExperimentUser, :count)
- end
- end
- end
- end
-
describe 'GET show.json' do
let(:pipeline) { create(:ci_pipeline, project: project) }
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index d30cc8cbfd9..53a7c2ca069 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -325,6 +325,18 @@ RSpec.describe Projects::ProjectMembersController do
expect(requester.reload.expires_at).not_to eq(expires_at.to_date)
end
+
+ it 'returns error status' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ end
+
+ it 'returns error message' do
+ subject
+
+ expect(json_response).to eq({ 'message' => 'Expires at cannot be a date in the past' })
+ end
end
context 'when set to a date in the future' do
diff --git a/spec/controllers/projects/security/configuration_controller_spec.rb b/spec/controllers/projects/security/configuration_controller_spec.rb
new file mode 100644
index 00000000000..ef255d1efd0
--- /dev/null
+++ b/spec/controllers/projects/security/configuration_controller_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::Security::ConfigurationController do
+ let(:project) { create(:project, :public) }
+ let(:user) { create(:user) }
+
+ before do
+ allow(controller).to receive(:ensure_security_and_compliance_enabled!)
+
+ sign_in(user)
+ end
+
+ describe 'GET show' do
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(secure_security_and_compliance_configuration_page_on_ce: false)
+ end
+
+ it 'renders not found' do
+ get :show, params: { namespace_id: project.namespace, project_id: project }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when feature flag is enabled' do
+ context 'when user has guest access' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'denies access' do
+ get :show, params: { namespace_id: project.namespace, project_id: project }
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'when user has developer access' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'grants access' do
+ get :show, params: { namespace_id: project.namespace, project_id: project }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:show)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/templates_controller_spec.rb b/spec/controllers/projects/templates_controller_spec.rb
index 01593f4133c..fe282baf769 100644
--- a/spec/controllers/projects/templates_controller_spec.rb
+++ b/spec/controllers/projects/templates_controller_spec.rb
@@ -165,7 +165,8 @@ RSpec.describe Projects::TemplatesController do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.size).to eq(2)
- expect(json_response).to match(expected_template_names)
+ expect(json_response.size).to eq(2)
+ expect(json_response.map { |x| x.slice('name') }).to match(expected_template_names)
end
it 'fails for user with no access' do
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index a611ac16cd9..1e4ec48b119 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -5,6 +5,7 @@ require('spec_helper')
RSpec.describe ProjectsController do
include ExternalAuthorizationServiceHelpers
include ProjectForksHelper
+ using RSpec::Parameterized::TableSyntax
let_it_be(:project, reload: true) { create(:project, service_desk_enabled: false) }
let_it_be(:public_project) { create(:project, :public) }
@@ -324,14 +325,39 @@ RSpec.describe ProjectsController do
end
end
- context "redirection from http://someproject.git" do
- it 'redirects to project page (format.html)' do
- project = create(:project, :public)
+ context 'redirection from http://someproject.git' do
+ where(:user_type, :project_visibility, :expected_redirect) do
+ :anonymous | :public | :redirect_to_project
+ :anonymous | :internal | :redirect_to_signup
+ :anonymous | :private | :redirect_to_signup
- get :show, params: { namespace_id: project.namespace, id: project }, format: :git
+ :signed_in | :public | :redirect_to_project
+ :signed_in | :internal | :redirect_to_project
+ :signed_in | :private | nil
- expect(response).to have_gitlab_http_status(:found)
- expect(response).to redirect_to(namespace_project_path)
+ :member | :public | :redirect_to_project
+ :member | :internal | :redirect_to_project
+ :member | :private | :redirect_to_project
+ end
+
+ with_them do
+ let(:redirect_to_signup) { new_user_session_path }
+ let(:redirect_to_project) { project_path(project) }
+
+ let(:expected_status) { expected_redirect ? :found : :not_found }
+
+ before do
+ project.update!(visibility: project_visibility.to_s)
+ project.team.add_user(user, :guest) if user_type == :member
+ sign_in(user) unless user_type == :anonymous
+ end
+
+ it 'returns the expected status' do
+ get :show, params: { namespace_id: project.namespace, id: project }, format: :git
+
+ expect(response).to have_gitlab_http_status(expected_status)
+ expect(response).to redirect_to(send(expected_redirect)) if expected_status == :found
+ end
end
end
@@ -384,6 +410,29 @@ RSpec.describe ProjectsController do
end
end
+ describe 'POST create' do
+ let!(:project_params) do
+ {
+ path: 'foo',
+ description: 'bar',
+ namespace_id: user.namespace.id,
+ visibility_level: Gitlab::VisibilityLevel::PUBLIC
+ }
+ end
+
+ before do
+ sign_in(user)
+ end
+
+ it 'tracks a created event for the new_project_readme experiment', :experiment do
+ expect(experiment(:new_project_readme)).to track(:created, property: 'blank').on_any_instance.with_context(
+ actor: user
+ )
+
+ post :create, params: { project: project_params }
+ end
+ end
+
describe 'POST #archive' do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
diff --git a/spec/controllers/registrations/experience_levels_controller_spec.rb b/spec/controllers/registrations/experience_levels_controller_spec.rb
index 015daba8682..79fa3f1474a 100644
--- a/spec/controllers/registrations/experience_levels_controller_spec.rb
+++ b/spec/controllers/registrations/experience_levels_controller_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Registrations::ExperienceLevelsController do
+ include AfterNextHelpers
+
let_it_be(:namespace) { create(:group, path: 'group-path' ) }
let_it_be(:user) { create(:user) }
@@ -19,20 +21,11 @@ RSpec.describe Registrations::ExperienceLevelsController do
context 'with an authenticated user' do
before do
sign_in(user)
- stub_experiment_for_subject(onboarding_issues: true)
end
it { is_expected.to have_gitlab_http_status(:ok) }
- it { is_expected.to render_template('layouts/devise_experimental_onboarding_issues') }
+ it { is_expected.to render_template('layouts/signup_onboarding') }
it { is_expected.to render_template(:show) }
-
- context 'when not part of the onboarding issues experiment' do
- before do
- stub_experiment_for_subject(onboarding_issues: false)
- end
-
- it { is_expected.to have_gitlab_http_status(:not_found) }
- end
end
end
@@ -45,17 +38,11 @@ RSpec.describe Registrations::ExperienceLevelsController do
end
context 'with an authenticated user' do
+ let_it_be(:project) { build(:project, namespace: namespace, creator: user, path: 'project-path') }
+ let_it_be(:issues_board) { build(:board, id: 123, project: project) }
+
before do
sign_in(user)
- stub_experiment_for_subject(onboarding_issues: true)
- end
-
- context 'when not part of the onboarding issues experiment' do
- before do
- stub_experiment_for_subject(onboarding_issues: false)
- end
-
- it { is_expected.to have_gitlab_http_status(:not_found) }
end
context 'when user is successfully updated' do
@@ -85,91 +72,57 @@ RSpec.describe Registrations::ExperienceLevelsController do
end
end
- describe 'redirection' do
- let(:project) { build(:project, namespace: namespace, creator: user, path: 'project-path') }
- let(:issues_board) { build(:board, id: 123, project: project) }
+ context 'when "Learn GitLab" project exists' do
+ let(:learn_gitlab_available?) { true }
before do
- stub_experiment_for_subject(
- onboarding_issues: true,
- default_to_issues_board: default_to_issues_board_xp?
- )
allow_next_instance_of(LearnGitlab) do |learn_gitlab|
allow(learn_gitlab).to receive(:available?).and_return(learn_gitlab_available?)
allow(learn_gitlab).to receive(:project).and_return(project)
allow(learn_gitlab).to receive(:board).and_return(issues_board)
+ allow(learn_gitlab).to receive(:label).and_return(double(id: 1))
end
end
- context 'when namespace_path param is missing' do
- let(:params) { super().merge(namespace_path: nil) }
-
- where(
- default_to_issues_board_xp?: [true, false],
- learn_gitlab_available?: [true, false]
- )
-
- with_them do
- it { is_expected.to redirect_to('/') }
- end
- end
-
- context 'when we have a namespace_path param' do
- using RSpec::Parameterized::TableSyntax
+ context 'redirection' do
+ context 'when namespace_path param is missing' do
+ let(:params) { super().merge(namespace_path: nil) }
- where(:default_to_issues_board_xp?, :learn_gitlab_available?, :path) do
- true | true | '/group-path/project-path/-/boards/123'
- true | false | '/group-path'
- false | true | '/group-path'
- false | false | '/group-path'
- end
-
- with_them do
- it { is_expected.to redirect_to(path) }
- end
- end
- end
+ where(
+ learn_gitlab_available?: [true, false]
+ )
- describe 'applying the chosen level' do
- context 'when a "Learn GitLab" project is available' do
- before do
- allow_next_instance_of(LearnGitlab) do |learn_gitlab|
- allow(learn_gitlab).to receive(:available?).and_return(true)
- allow(learn_gitlab).to receive(:label).and_return(double(id: 1))
+ with_them do
+ it { is_expected.to redirect_to('/') }
end
end
- context 'when novice' do
- let(:params) { super().merge(experience_level: :novice) }
-
- it 'adds a BoardLabel' do
- expect_next_instance_of(Boards::UpdateService) do |service|
- expect(service).to receive(:execute)
- end
+ context 'when we have a namespace_path param' do
+ using RSpec::Parameterized::TableSyntax
- subject
+ where(:learn_gitlab_available?, :path) do
+ true | '/group-path/project-path/-/boards/123'
+ false | '/group-path'
end
- end
-
- context 'when experienced' do
- let(:params) { super().merge(experience_level: :experienced) }
- it 'does not add a BoardLabel' do
- expect(Boards::UpdateService).not_to receive(:new)
-
- subject
+ with_them do
+ it { is_expected.to redirect_to(path) }
end
end
end
- context 'when no "Learn GitLab" project exists' do
+ context 'when novice' do
let(:params) { super().merge(experience_level: :novice) }
- before do
- allow_next_instance_of(LearnGitlab) do |learn_gitlab|
- allow(learn_gitlab).to receive(:available?).and_return(false)
- end
+ it 'adds a BoardLabel' do
+ expect_next(Boards::UpdateService).to receive(:execute)
+
+ subject
end
+ end
+
+ context 'when experienced' do
+ let(:params) { super().merge(experience_level: :experienced) }
it 'does not add a BoardLabel' do
expect(Boards::UpdateService).not_to receive(:new)
@@ -178,6 +131,20 @@ RSpec.describe Registrations::ExperienceLevelsController do
end
end
end
+
+ context 'when no "Learn GitLab" project exists' do
+ let(:params) { super().merge(experience_level: :novice) }
+
+ before do
+ allow_next(LearnGitlab).to receive(:available?).and_return(false)
+ end
+
+ it 'does not add a BoardLabel' do
+ expect(Boards::UpdateService).not_to receive(:new)
+
+ subject
+ end
+ end
end
context 'when user update fails' do
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index 737ec4f95c5..aac7c10d878 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -73,6 +73,18 @@ RSpec.describe RegistrationsController do
end
end
end
+
+ context 'audit events' do
+ context 'when not licensed' do
+ before do
+ stub_licensed_features(admin_audit_log: false)
+ end
+
+ it 'does not log any audit event' do
+ expect { subject }.not_to change(AuditEvent, :count)
+ end
+ end
+ end
end
context 'when the `require_admin_approval_after_user_signup` setting is turned off' do
diff --git a/spec/controllers/repositories/git_http_controller_spec.rb b/spec/controllers/repositories/git_http_controller_spec.rb
index 34052496871..d21f602f90c 100644
--- a/spec/controllers/repositories/git_http_controller_spec.rb
+++ b/spec/controllers/repositories/git_http_controller_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe Repositories::GitHttpController do
context 'when project_statistics_sync feature flag is disabled' do
before do
- stub_feature_flags(project_statistics_sync: false)
+ stub_feature_flags(project_statistics_sync: false, disable_git_http_fetch_writes: false)
end
it 'updates project statistics async for projects' do
@@ -47,17 +47,40 @@ RSpec.describe Repositories::GitHttpController do
end
it 'updates project statistics sync for projects' do
+ stub_feature_flags(disable_git_http_fetch_writes: false)
+
expect { send_request }.to change {
Projects::DailyStatisticsFinder.new(container).total_fetch_count
}.from(0).to(1)
end
- it 'records an onboarding progress action' do
- expect_next_instance_of(OnboardingProgressService) do |service|
- expect(service).to receive(:execute).with(action: :git_read)
+ it_behaves_like 'records an onboarding progress action', :git_read do
+ let(:namespace) { project.namespace }
+
+ subject { send_request }
+
+ before do
+ stub_feature_flags(disable_git_http_fetch_writes: false)
+ end
+ end
+
+ context 'when disable_git_http_fetch_writes is enabled' do
+ before do
+ stub_feature_flags(disable_git_http_fetch_writes: true)
+ end
+
+ it 'does not increment statistics' do
+ expect(Projects::FetchStatisticsIncrementService).not_to receive(:new)
+ expect(ProjectDailyStatisticsWorker).not_to receive(:perform_async)
+
+ send_request
end
- send_request
+ it 'does not record onboarding progress' do
+ expect(OnboardingProgressService).not_to receive(:new)
+
+ send_request
+ end
end
end
end
diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb
index bbd39fd4c83..95cea10f0d0 100644
--- a/spec/controllers/search_controller_spec.rb
+++ b/spec/controllers/search_controller_spec.rb
@@ -5,278 +5,296 @@ require 'spec_helper'
RSpec.describe SearchController do
include ExternalAuthorizationServiceHelpers
- let(:user) { create(:user) }
+ context 'authorized user' do
+ let(:user) { create(:user) }
- before do
- sign_in(user)
- end
-
- shared_examples_for 'when the user cannot read cross project' do |action, params|
before do
- allow(Ability).to receive(:allowed?).and_call_original
- allow(Ability).to receive(:allowed?)
- .with(user, :read_cross_project, :global) { false }
+ sign_in(user)
end
- it 'blocks access without a project_id' do
- get action, params: params
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
+ shared_examples_for 'when the user cannot read cross project' do |action, params|
+ before do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?)
+ .with(user, :read_cross_project, :global) { false }
+ end
- it 'allows access with a project_id' do
- get action, params: params.merge(project_id: create(:project, :public).id)
+ it 'blocks access without a project_id' do
+ get action, params: params
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
- shared_examples_for 'with external authorization service enabled' do |action, params|
- let(:project) { create(:project, namespace: user.namespace) }
- let(:note) { create(:note_on_issue, project: project) }
+ it 'allows access with a project_id' do
+ get action, params: params.merge(project_id: create(:project, :public).id)
- before do
- enable_external_authorization_service_check
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
- it 'renders a 403 when no project is given' do
- get action, params: params
+ shared_examples_for 'with external authorization service enabled' do |action, params|
+ let(:project) { create(:project, namespace: user.namespace) }
+ let(:note) { create(:note_on_issue, project: project) }
- expect(response).to have_gitlab_http_status(:forbidden)
- end
+ before do
+ enable_external_authorization_service_check
+ end
- it 'renders a 200 when a project was set' do
- get action, params: params.merge(project_id: project.id)
+ it 'renders a 403 when no project is given' do
+ get action, params: params
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
- describe 'GET #show' do
- it_behaves_like 'when the user cannot read cross project', :show, { search: 'hello' } do
- it 'still allows accessing the search page' do
- get :show
+ it 'renders a 200 when a project was set' do
+ get action, params: params.merge(project_id: project.id)
expect(response).to have_gitlab_http_status(:ok)
end
end
- it_behaves_like 'with external authorization service enabled', :show, { search: 'hello' }
+ describe 'GET #show' do
+ it_behaves_like 'when the user cannot read cross project', :show, { search: 'hello' } do
+ it 'still allows accessing the search page' do
+ get :show
- context 'uses the right partials depending on scope' do
- using RSpec::Parameterized::TableSyntax
- render_views
-
- let_it_be(:project) { create(:project, :public, :repository, :wiki_repo) }
-
- before do
- expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
- subject { get(:show, params: { project_id: project.id, scope: scope, search: 'merge' }) }
+ it_behaves_like 'with external authorization service enabled', :show, { search: 'hello' }
- where(:partial, :scope) do
- '_blob' | :blobs
- '_wiki_blob' | :wiki_blobs
- '_commit' | :commits
- end
+ context 'uses the right partials depending on scope' do
+ using RSpec::Parameterized::TableSyntax
+ render_views
- with_them do
- it do
- project_wiki = create(:project_wiki, project: project, user: user)
- create(:wiki_page, wiki: project_wiki, title: 'merge', content: 'merge')
+ let_it_be(:project) { create(:project, :public, :repository, :wiki_repo) }
- expect(subject).to render_template("search/results/#{partial}")
+ before do
+ expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
end
- end
- end
- context 'global search' do
- using RSpec::Parameterized::TableSyntax
- render_views
+ subject { get(:show, params: { project_id: project.id, scope: scope, search: 'merge' }) }
- context 'when block_anonymous_global_searches is disabled' do
- before do
- stub_feature_flags(block_anonymous_global_searches: false)
+ where(:partial, :scope) do
+ '_blob' | :blobs
+ '_wiki_blob' | :wiki_blobs
+ '_commit' | :commits
end
- it 'omits pipeline status from load' do
- project = create(:project, :public)
- expect(Gitlab::Cache::Ci::ProjectPipelineStatus).not_to receive(:load_in_batch_for_projects)
+ with_them do
+ it do
+ project_wiki = create(:project_wiki, project: project, user: user)
+ create(:wiki_page, wiki: project_wiki, title: 'merge', content: 'merge')
- get :show, params: { scope: 'projects', search: project.name }
-
- expect(assigns[:search_objects].first).to eq project
+ expect(subject).to render_template("search/results/#{partial}")
+ end
end
+ end
- context 'check search term length' do
- let(:search_queries) do
- char_limit = SearchService::SEARCH_CHAR_LIMIT
- term_limit = SearchService::SEARCH_TERM_LIMIT
- {
- chars_under_limit: ('a' * (char_limit - 1)),
- chars_over_limit: ('a' * (char_limit + 1)),
- terms_under_limit: ('abc ' * (term_limit - 1)),
- terms_over_limit: ('abc ' * (term_limit + 1))
- }
+ context 'global search' do
+ using RSpec::Parameterized::TableSyntax
+ render_views
+
+ context 'when block_anonymous_global_searches is disabled' do
+ before do
+ stub_feature_flags(block_anonymous_global_searches: false)
end
- where(:string_name, :expectation) do
- :chars_under_limit | :not_to_set_flash
- :chars_over_limit | :set_chars_flash
- :terms_under_limit | :not_to_set_flash
- :terms_over_limit | :set_terms_flash
+ it 'omits pipeline status from load' do
+ project = create(:project, :public)
+ expect(Gitlab::Cache::Ci::ProjectPipelineStatus).not_to receive(:load_in_batch_for_projects)
+
+ get :show, params: { scope: 'projects', search: project.name }
+
+ expect(assigns[:search_objects].first).to eq project
end
- with_them do
- it do
- get :show, params: { scope: 'projects', search: search_queries[string_name] }
-
- case expectation
- when :not_to_set_flash
- expect(controller).not_to set_flash[:alert]
- when :set_chars_flash
- expect(controller).to set_flash[:alert].to(/characters/)
- when :set_terms_flash
- expect(controller).to set_flash[:alert].to(/terms/)
+ context 'check search term length' do
+ let(:search_queries) do
+ char_limit = SearchService::SEARCH_CHAR_LIMIT
+ term_limit = SearchService::SEARCH_TERM_LIMIT
+ {
+ chars_under_limit: ('a' * (char_limit - 1)),
+ chars_over_limit: ('a' * (char_limit + 1)),
+ terms_under_limit: ('abc ' * (term_limit - 1)),
+ terms_over_limit: ('abc ' * (term_limit + 1))
+ }
+ end
+
+ where(:string_name, :expectation) do
+ :chars_under_limit | :not_to_set_flash
+ :chars_over_limit | :set_chars_flash
+ :terms_under_limit | :not_to_set_flash
+ :terms_over_limit | :set_terms_flash
+ end
+
+ with_them do
+ it do
+ get :show, params: { scope: 'projects', search: search_queries[string_name] }
+
+ case expectation
+ when :not_to_set_flash
+ expect(controller).not_to set_flash[:alert]
+ when :set_chars_flash
+ expect(controller).to set_flash[:alert].to(/characters/)
+ when :set_terms_flash
+ expect(controller).to set_flash[:alert].to(/terms/)
+ end
end
end
end
end
- end
- context 'when block_anonymous_global_searches is enabled' do
- context 'for unauthenticated user' do
- before do
- sign_out(user)
- end
+ context 'when block_anonymous_global_searches is enabled' do
+ context 'for unauthenticated user' do
+ before do
+ sign_out(user)
+ end
- it 'redirects to login page' do
- get :show, params: { scope: 'projects', search: '*' }
+ it 'redirects to login page' do
+ get :show, params: { scope: 'projects', search: '*' }
- expect(response).to redirect_to new_user_session_path
+ expect(response).to redirect_to new_user_session_path
+ end
end
- end
- context 'for authenticated user' do
- it 'succeeds' do
- get :show, params: { scope: 'projects', search: '*' }
+ context 'for authenticated user' do
+ it 'succeeds' do
+ get :show, params: { scope: 'projects', search: '*' }
- expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
end
end
- end
- it 'finds issue comments' do
- project = create(:project, :public)
- note = create(:note_on_issue, project: project)
+ it 'finds issue comments' do
+ project = create(:project, :public)
+ note = create(:note_on_issue, project: project)
- get :show, params: { project_id: project.id, scope: 'notes', search: note.note }
-
- expect(assigns[:search_objects].first).to eq note
- end
+ get :show, params: { project_id: project.id, scope: 'notes', search: note.note }
- context 'unique users tracking' do
- before do
- allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event)
+ expect(assigns[:search_objects].first).to eq note
end
- it_behaves_like 'tracking unique hll events', :search_track_unique_users do
- subject(:request) { get :show, params: { scope: 'projects', search: 'term' } }
+ context 'unique users tracking' do
+ before do
+ allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event)
+ end
+
+ it_behaves_like 'tracking unique hll events' do
+ subject(:request) { get :show, params: { scope: 'projects', search: 'term' } }
- let(:target_id) { 'i_search_total' }
- let(:expected_type) { instance_of(String) }
+ let(:target_id) { 'i_search_total' }
+ let(:expected_type) { instance_of(String) }
+ end
end
- end
- context 'on restricted projects' do
- context 'when signed out' do
- before do
- sign_out(user)
+ context 'on restricted projects' do
+ context 'when signed out' do
+ before do
+ sign_out(user)
+ end
+
+ it "doesn't expose comments on issues" do
+ project = create(:project, :public, :issues_private)
+ note = create(:note_on_issue, project: project)
+
+ get :show, params: { project_id: project.id, scope: 'notes', search: note.note }
+
+ expect(assigns[:search_objects].count).to eq(0)
+ end
end
- it "doesn't expose comments on issues" do
- project = create(:project, :public, :issues_private)
- note = create(:note_on_issue, project: project)
+ it "doesn't expose comments on merge_requests" do
+ project = create(:project, :public, :merge_requests_private)
+ note = create(:note_on_merge_request, project: project)
get :show, params: { project_id: project.id, scope: 'notes', search: note.note }
expect(assigns[:search_objects].count).to eq(0)
end
- end
- it "doesn't expose comments on merge_requests" do
- project = create(:project, :public, :merge_requests_private)
- note = create(:note_on_merge_request, project: project)
+ it "doesn't expose comments on snippets" do
+ project = create(:project, :public, :snippets_private)
+ note = create(:note_on_project_snippet, project: project)
- get :show, params: { project_id: project.id, scope: 'notes', search: note.note }
+ get :show, params: { project_id: project.id, scope: 'notes', search: note.note }
- expect(assigns[:search_objects].count).to eq(0)
+ expect(assigns[:search_objects].count).to eq(0)
+ end
end
+ end
- it "doesn't expose comments on snippets" do
- project = create(:project, :public, :snippets_private)
- note = create(:note_on_project_snippet, project: project)
-
- get :show, params: { project_id: project.id, scope: 'notes', search: note.note }
+ describe 'GET #count' do
+ it_behaves_like 'when the user cannot read cross project', :count, { search: 'hello', scope: 'projects' }
+ it_behaves_like 'with external authorization service enabled', :count, { search: 'hello', scope: 'projects' }
- expect(assigns[:search_objects].count).to eq(0)
- end
- end
- end
+ it 'returns the result count for the given term and scope' do
+ create(:project, :public, name: 'hello world')
+ create(:project, :public, name: 'foo bar')
- describe 'GET #count' do
- it_behaves_like 'when the user cannot read cross project', :count, { search: 'hello', scope: 'projects' }
- it_behaves_like 'with external authorization service enabled', :count, { search: 'hello', scope: 'projects' }
+ get :count, params: { search: 'hello', scope: 'projects' }
- it 'returns the result count for the given term and scope' do
- create(:project, :public, name: 'hello world')
- create(:project, :public, name: 'foo bar')
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq({ 'count' => '1' })
+ end
- get :count, params: { search: 'hello', scope: 'projects' }
+ it 'raises an error if search term is missing' do
+ expect do
+ get :count, params: { scope: 'projects' }
+ end.to raise_error(ActionController::ParameterMissing)
+ end
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to eq({ 'count' => '1' })
+ it 'raises an error if search scope is missing' do
+ expect do
+ get :count, params: { search: 'hello' }
+ end.to raise_error(ActionController::ParameterMissing)
+ end
end
- it 'raises an error if search term is missing' do
- expect do
- get :count, params: { scope: 'projects' }
- end.to raise_error(ActionController::ParameterMissing)
+ describe 'GET #autocomplete' do
+ it_behaves_like 'when the user cannot read cross project', :autocomplete, { term: 'hello' }
+ it_behaves_like 'with external authorization service enabled', :autocomplete, { term: 'hello' }
end
- it 'raises an error if search scope is missing' do
- expect do
- get :count, params: { search: 'hello' }
- end.to raise_error(ActionController::ParameterMissing)
- end
- end
+ describe '#append_info_to_payload' do
+ it 'appends search metadata for logging' do
+ last_payload = nil
+ original_append_info_to_payload = controller.method(:append_info_to_payload)
- describe 'GET #autocomplete' do
- it_behaves_like 'when the user cannot read cross project', :autocomplete, { term: 'hello' }
- it_behaves_like 'with external authorization service enabled', :autocomplete, { term: 'hello' }
- end
+ expect(controller).to receive(:append_info_to_payload) do |payload|
+ original_append_info_to_payload.call(payload)
+ last_payload = payload
+ end
- describe '#append_info_to_payload' do
- it 'appends search metadata for logging' do
- last_payload = nil
- original_append_info_to_payload = controller.method(:append_info_to_payload)
+ get :show, params: { scope: 'issues', search: 'hello world', group_id: '123', project_id: '456', confidential: true, state: true, force_search_results: true }
- expect(controller).to receive(:append_info_to_payload) do |payload|
- original_append_info_to_payload.call(payload)
- last_payload = payload
+ expect(last_payload[:metadata]['meta.search.group_id']).to eq('123')
+ expect(last_payload[:metadata]['meta.search.project_id']).to eq('456')
+ expect(last_payload[:metadata]).not_to have_key('meta.search.search')
+ expect(last_payload[:metadata]['meta.search.scope']).to eq('issues')
+ expect(last_payload[:metadata]['meta.search.force_search_results']).to eq('true')
+ expect(last_payload[:metadata]['meta.search.filters.confidential']).to eq('true')
+ expect(last_payload[:metadata]['meta.search.filters.state']).to eq('true')
end
+ end
+ end
- get :show, params: { scope: 'issues', search: 'hello world', group_id: '123', project_id: '456', confidential: true, state: true, force_search_results: true }
+ context 'unauthorized user' do
+ describe 'GET #opensearch' do
+ render_views
+
+ it 'renders xml' do
+ get :opensearch, format: :xml
+
+ doc = Nokogiri::XML.parse(response.body)
- expect(last_payload[:metadata]['meta.search.group_id']).to eq('123')
- expect(last_payload[:metadata]['meta.search.project_id']).to eq('456')
- expect(last_payload[:metadata]).not_to have_key('meta.search.search')
- expect(last_payload[:metadata]['meta.search.scope']).to eq('issues')
- expect(last_payload[:metadata]['meta.search.force_search_results']).to eq('true')
- expect(last_payload[:metadata]['meta.search.filters.confidential']).to eq('true')
- expect(last_payload[:metadata]['meta.search.filters.state']).to eq('true')
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(doc.css('OpenSearchDescription ShortName').text).to eq('GitLab')
+ expect(doc.css('OpenSearchDescription *').map(&:name)).to eq(%w[ShortName Description InputEncoding Image Url SearchForm])
+ end
end
end
end
diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
index 51cecb348c8..50d6ac8f23d 100644
--- a/spec/controllers/snippets_controller_spec.rb
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -173,7 +173,7 @@ RSpec.describe SnippetsController do
expect(response).to have_gitlab_http_status(:ok)
end
- it_behaves_like 'tracking unique hll events', :usage_data_i_snippets_show do
+ it_behaves_like 'tracking unique hll events' do
subject(:request) { get :show, params: { id: public_snippet.to_param } }
let(:target_id) { 'i_snippets_show' }
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
index 3087beb1326..b2c77a06f19 100644
--- a/spec/db/schema_spec.rb
+++ b/spec/db/schema_spec.rb
@@ -26,6 +26,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 runner_id trigger_request_id user_id],
+ ci_namespace_monthly_usages: %w[namespace_id],
ci_pipelines: %w[user_id],
ci_runner_projects: %w[runner_id],
ci_trigger_requests: %w[commit_id],
@@ -184,6 +185,7 @@ RSpec.describe 'Database schema' do
"ApplicationSetting" => %w[repository_storages_weighted],
"AlertManagement::Alert" => %w[payload],
"Ci::BuildMetadata" => %w[config_options config_variables],
+ "ExperimentSubject" => %w[context],
"ExperimentUser" => %w[context],
"Geo::Event" => %w[payload],
"GeoNodeStatus" => %w[status],
diff --git a/spec/deprecation_toolkit_env.rb b/spec/deprecation_toolkit_env.rb
index bc90f67f0db..d2ff2d2cb37 100644
--- a/spec/deprecation_toolkit_env.rb
+++ b/spec/deprecation_toolkit_env.rb
@@ -1,18 +1,84 @@
# frozen_string_literal: true
-if ENV.key?('RECORD_DEPRECATIONS')
- require 'deprecation_toolkit'
- require 'deprecation_toolkit/rspec'
- DeprecationToolkit::Configuration.test_runner = :rspec
- DeprecationToolkit::Configuration.deprecation_path = 'deprecations'
- DeprecationToolkit::Configuration.behavior = DeprecationToolkit::Behaviors::Record
-
- # Enable ruby deprecations for keywords, it's suppressed by default in Ruby 2.7.2
- Warning[:deprecated] = true
-
- kwargs_warnings = [
- # Taken from https://github.com/jeremyevans/ruby-warning/blob/1.1.0/lib/warning.rb#L18
+require 'deprecation_toolkit'
+require 'deprecation_toolkit/rspec'
+
+module DeprecationToolkitEnv
+ module DeprecationBehaviors
+ class SelectiveRaise
+ attr_reader :disallowed_deprecations_proc
+
+ class RaiseDisallowedDeprecation < StandardError
+ def initialize(test, current_deprecations)
+ message = <<~EOF
+ Disallowed deprecations detected while running test #{test}:
+
+ #{current_deprecations.deprecations.join("\n")}
+ EOF
+
+ super(message)
+ end
+ end
+
+ def initialize(disallowed_deprecations_proc)
+ @disallowed_deprecations_proc = disallowed_deprecations_proc
+ end
+
+ # Note: trigger does not get called if the current_deprecations matches recorded_deprecations
+ # See https://github.com/Shopify/deprecation_toolkit/blob/2398f38acb62220fb79a6cd720f61d9cea26bc06/lib/deprecation_toolkit/test_triggerer.rb#L8-L11
+ def trigger(test, current_deprecations, recorded_deprecations)
+ if selected_for_raise?(current_deprecations)
+ raise RaiseDisallowedDeprecation.new(test, current_deprecations)
+ elsif ENV['RECORD_DEPRECATIONS']
+ record(test, current_deprecations, recorded_deprecations)
+ end
+ end
+
+ private
+
+ def selected_for_raise?(current_deprecations)
+ disallowed_deprecations_proc.call(current_deprecations.deprecations_without_stacktrace)
+ end
+
+ def record(test, current_deprecations, recorded_deprecations)
+ ::DeprecationToolkit::Behaviors::Record.trigger(test, current_deprecations, recorded_deprecations)
+ end
+ end
+ end
+
+ # Taken from https://github.com/jeremyevans/ruby-warning/blob/1.1.0/lib/warning.rb#L18
+ def self.kwargs_warning
%r{warning: (?:Using the last argument (?:for `.+' )?as keyword parameters is deprecated; maybe \*\* should be added to the call|Passing the keyword argument (?:for `.+' )?as the last hash parameter is deprecated|Splitting the last argument (?:for `.+' )?into positional and keyword parameters is deprecated|The called method (?:`.+' )?is defined here)\n\z}
- ]
- DeprecationToolkit::Configuration.warnings_treated_as_deprecation = kwargs_warnings
+ end
+
+ # Allow these Gem paths to trigger keyword warnings as we upgrade these gems
+ # one by one
+ def self.allowed_kwarg_warning_paths
+ %w[
+ activerecord-6.0.3.4/lib/active_record/migration.rb
+ devise-4.7.3/lib/devise/test/controller_helpers.rb
+ activesupport-6.0.3.4/lib/active_support/cache.rb
+ batch-loader-1.4.0/lib/batch_loader/graphql.rb
+ carrierwave-1.3.1/lib/carrierwave/sanitized_file.rb
+ activerecord-6.0.3.4/lib/active_record/relation.rb
+ ]
+ end
+
+ def self.configure!
+ # Enable ruby deprecations for keywords, it's suppressed by default in Ruby 2.7.2
+ Warning[:deprecated] = true
+
+ DeprecationToolkit::Configuration.test_runner = :rspec
+ DeprecationToolkit::Configuration.deprecation_path = 'deprecations'
+ DeprecationToolkit::Configuration.warnings_treated_as_deprecation = [kwargs_warning]
+
+ disallowed_deprecations = -> (deprecations) do
+ deprecations.any? do |deprecation|
+ kwargs_warning.match?(deprecation) &&
+ allowed_kwarg_warning_paths.none? { |path| deprecation.include?(path) }
+ end
+ end
+
+ DeprecationToolkit::Configuration.behavior = DeprecationBehaviors::SelectiveRaise.new(disallowed_deprecations)
+ end
end
diff --git a/spec/experiments/application_experiment/cache_spec.rb b/spec/experiments/application_experiment/cache_spec.rb
index a420d557155..4caa91e6ac4 100644
--- a/spec/experiments/application_experiment/cache_spec.rb
+++ b/spec/experiments/application_experiment/cache_spec.rb
@@ -51,16 +51,4 @@ RSpec.describe ApplicationExperiment::Cache do
'invalid call to clear a non-hash cache key'
)
end
-
- context "when the :caching_experiments feature is disabled" do
- before do
- stub_feature_flags(caching_experiments: false)
- end
-
- it "doesn't write to the cache" do
- subject.write(key_field, 'value')
-
- expect(subject.read(key_field)).to be_nil
- end
- end
end
diff --git a/spec/experiments/application_experiment_spec.rb b/spec/experiments/application_experiment_spec.rb
index ece52d37351..501d344e920 100644
--- a/spec/experiments/application_experiment_spec.rb
+++ b/spec/experiments/application_experiment_spec.rb
@@ -2,8 +2,60 @@
require 'spec_helper'
-RSpec.describe ApplicationExperiment do
- subject { described_class.new(:stub) }
+RSpec.describe ApplicationExperiment, :experiment do
+ subject { described_class.new('namespaced/stub') }
+
+ let(:feature_definition) do
+ { name: 'namespaced_stub', type: 'experiment', group: 'group::adoption', default_enabled: false }
+ end
+
+ around do |example|
+ Feature::Definition.definitions[:namespaced_stub] = Feature::Definition.new('namespaced_stub.yml', feature_definition)
+ example.run
+ Feature::Definition.definitions.delete(:namespaced_stub)
+ end
+
+ before do
+ allow(subject).to receive(:enabled?).and_return(true)
+ end
+
+ it "naively assumes a 1x1 relationship to feature flags for tests" do
+ expect(Feature).to receive(:persist_used!).with('namespaced_stub')
+
+ described_class.new('namespaced/stub')
+ end
+
+ describe "enabled" do
+ before do
+ allow(subject).to receive(:enabled?).and_call_original
+
+ allow(Feature::Definition).to receive(:get).and_return('_instance_')
+ allow(Gitlab).to receive(:dev_env_or_com?).and_return(true)
+ allow(Feature).to receive(:get).and_return(double(state: :on))
+ end
+
+ it "is enabled when all criteria are met" do
+ expect(subject).to be_enabled
+ end
+
+ it "isn't enabled if the feature definition doesn't exist" do
+ expect(Feature::Definition).to receive(:get).with('namespaced_stub').and_return(nil)
+
+ expect(subject).not_to be_enabled
+ end
+
+ it "isn't enabled if we're not in dev or dotcom environments" do
+ expect(Gitlab).to receive(:dev_env_or_com?).and_return(false)
+
+ expect(subject).not_to be_enabled
+ end
+
+ it "isn't enabled if the feature flag state is :off" do
+ expect(Feature).to receive(:get).with('namespaced_stub').and_return(double(state: :off))
+
+ expect(subject).not_to be_enabled
+ end
+ end
describe "publishing results" do
it "tracks the assignment" do
@@ -16,9 +68,9 @@ RSpec.describe ApplicationExperiment do
expect(Gon.global).to receive(:push).with(
{
experiment: {
- 'stub' => { # string key because it can be namespaced
- experiment: 'stub',
- key: 'e8f65fd8d973f9985dc7ea3cf1614ae1',
+ 'namespaced/stub' => { # string key because it can be namespaced
+ experiment: 'namespaced/stub',
+ key: '86208ac54ca798e11f127e8b23ec396a',
variant: 'control'
}
}
@@ -31,8 +83,8 @@ RSpec.describe ApplicationExperiment do
end
describe "tracking events", :snowplow do
- it "doesn't track if excluded" do
- subject.exclude { true }
+ it "doesn't track if we shouldn't track" do
+ allow(subject).to receive(:should_track?).and_return(false)
subject.track(:action)
@@ -45,7 +97,7 @@ RSpec.describe ApplicationExperiment do
])
expect_snowplow_event(
- category: 'stub',
+ category: 'namespaced/stub',
action: 'action',
property: '_property_',
context: [
@@ -55,7 +107,7 @@ RSpec.describe ApplicationExperiment do
},
{
schema: 'iglu:com.gitlab/gitlab_experiment/jsonschema/0-3-0',
- data: { experiment: 'stub', key: 'e8f65fd8d973f9985dc7ea3cf1614ae1', variant: 'control' }
+ data: { experiment: 'namespaced/stub', key: '86208ac54ca798e11f127e8b23ec396a', variant: 'control' }
}
]
)
@@ -63,18 +115,77 @@ RSpec.describe ApplicationExperiment do
end
describe "variant resolution" do
- it "returns nil when not rolled out" do
- stub_feature_flags(stub: false)
+ context "when using the default feature flag percentage rollout" do
+ it "uses the default value as specified in the yaml" do
+ expect(Feature).to receive(:enabled?).with('namespaced_stub', subject, type: :experiment, default_enabled: :yaml)
+
+ expect(subject.variant.name).to eq('control')
+ end
+
+ it "returns nil when not rolled out" do
+ stub_feature_flags(namespaced_stub: false)
+
+ expect(subject.variant.name).to eq('control')
+ end
+
+ context "when rolled out to 100%" do
+ it "returns the first variant name" do
+ subject.try(:variant1) {}
+ subject.try(:variant2) {}
- expect(subject.variant.name).to eq('control')
+ expect(subject.variant.name).to eq('variant1')
+ end
+ end
end
- context "when rolled out to 100%" do
- it "returns the first variant name" do
- subject.try(:variant1) {}
- subject.try(:variant2) {}
+ context "when using the round_robin strategy", :clean_gitlab_redis_shared_state do
+ context "when variants aren't supplied" do
+ subject :inheriting_class do
+ Class.new(described_class) do
+ def rollout_strategy
+ :round_robin
+ end
+ end.new('namespaced/stub')
+ end
+
+ it "raises an error" do
+ expect { inheriting_class.variants }.to raise_error(NotImplementedError)
+ end
+ end
+
+ context "when variants are supplied" do
+ let(:inheriting_class) do
+ Class.new(described_class) do
+ def rollout_strategy
+ :round_robin
+ end
+
+ def variants
+ %i[variant1 variant2 control]
+ end
+ end
+ end
+
+ it "proves out round robin in variant selection", :aggregate_failures do
+ instance_1 = inheriting_class.new('namespaced/stub')
+ allow(instance_1).to receive(:enabled?).and_return(true)
+ instance_2 = inheriting_class.new('namespaced/stub')
+ allow(instance_2).to receive(:enabled?).and_return(true)
+ instance_3 = inheriting_class.new('namespaced/stub')
+ allow(instance_3).to receive(:enabled?).and_return(true)
+
+ instance_1.try {}
+
+ expect(instance_1.variant.name).to eq('variant2')
+
+ instance_2.try {}
+
+ expect(instance_2.variant.name).to eq('control')
+
+ instance_3.try {}
- expect(subject.variant.name).to eq('variant1')
+ expect(instance_3.variant.name).to eq('variant1')
+ end
end
end
end
@@ -105,7 +216,7 @@ RSpec.describe ApplicationExperiment do
# every control variant assigned, we'd inflate the cache size and
# wouldn't be able to roll out to subjects that we'd already assigned to
# the control.
- stub_feature_flags(stub: false) # simulate being not rolled out
+ stub_feature_flags(namespaced_stub: false) # simulate being not rolled out
expect(subject.variant.name).to eq('control') # if we ask, it should be control
diff --git a/spec/experiments/members/invite_email_experiment_spec.rb b/spec/experiments/members/invite_email_experiment_spec.rb
new file mode 100644
index 00000000000..4376c021385
--- /dev/null
+++ b/spec/experiments/members/invite_email_experiment_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Members::InviteEmailExperiment do
+ subject :invite_email do
+ experiment('members/invite_email', actor: double('Member', created_by: double('User', avatar_url: '_avatar_url_')))
+ end
+
+ before do
+ allow(invite_email).to receive(:enabled?).and_return(true)
+ end
+
+ describe "#rollout_strategy" do
+ it "resolves to round_robin" do
+ expect(invite_email.rollout_strategy).to eq(:round_robin)
+ end
+ end
+
+ describe "#variants" do
+ it "has all the expected variants" do
+ expect(invite_email.variants).to match(%i[avatar permission_info control])
+ end
+ end
+
+ describe "exclusions", :experiment do
+ it "excludes when created by is nil" do
+ expect(experiment('members/invite_email')).to exclude(actor: double(created_by: nil))
+ end
+
+ it "excludes when avatar_url is nil" do
+ member_without_avatar_url = double('Member', created_by: double('User', avatar_url: nil))
+
+ expect(experiment('members/invite_email')).to exclude(actor: member_without_avatar_url)
+ end
+ end
+end
diff --git a/spec/experiments/new_project_readme_experiment_spec.rb b/spec/experiments/new_project_readme_experiment_spec.rb
new file mode 100644
index 00000000000..17e28cf6e7f
--- /dev/null
+++ b/spec/experiments/new_project_readme_experiment_spec.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe NewProjectReadmeExperiment, :experiment do
+ subject { described_class.new(actor: actor) }
+
+ let(:actor) { User.new(id: 42, created_at: Time.current) }
+
+ before do
+ stub_experiments(new_project_readme: :control)
+ end
+
+ describe "exclusions" do
+ let(:threshold) { described_class::MAX_ACCOUNT_AGE }
+
+ it { is_expected.to exclude(actor: User.new(created_at: (threshold + 1.minute).ago)) }
+ it { is_expected.not_to exclude(actor: User.new(created_at: (threshold - 1.minute).ago)) }
+ end
+
+ describe "the control behavior" do
+ subject { described_class.new(actor: actor).run(:control) }
+
+ it { is_expected.to be false }
+ end
+
+ describe "the candidate behavior" do
+ subject { described_class.new(actor: actor).run(:candidate) }
+
+ it { is_expected.to be true }
+ end
+
+ context "when tracking initial writes" do
+ let!(:project) { create(:project, :repository) }
+
+ def stub_gitaly_count(count = 1)
+ allow(Gitlab::GitalyClient).to receive(:call).and_call_original
+ allow(Gitlab::GitalyClient).to receive(:call).with(anything, :commit_service, :count_commits, anything, anything)
+ .and_return(double(count: count))
+ end
+
+ before do
+ stub_gitaly_count
+ end
+
+ it "tracks an event for the first commit on a project with a repository" do
+ expect(subject).to receive(:track).with(:write, property: project.created_at.to_s, value: 1).and_call_original
+
+ subject.track_initial_writes(project)
+ end
+
+ it "tracks an event for the second commit on a project with a repository" do
+ stub_gitaly_count(2)
+
+ expect(subject).to receive(:track).with(:write, property: project.created_at.to_s, value: 2).and_call_original
+
+ subject.track_initial_writes(project)
+ end
+
+ it "doesn't track if the repository has more then 2 commits" do
+ stub_gitaly_count(3)
+
+ expect(subject).not_to receive(:track)
+
+ subject.track_initial_writes(project)
+ end
+
+ it "doesn't track when we generally shouldn't" do
+ allow(subject).to receive(:should_track?).and_return(false)
+
+ expect(subject).not_to receive(:track)
+
+ subject.track_initial_writes(project)
+ end
+
+ it "doesn't track if the project is older" do
+ expect(project).to receive(:created_at).and_return(described_class::EXPERIMENT_START_DATE - 1.minute)
+
+ expect(subject).not_to receive(:track)
+
+ subject.track_initial_writes(project)
+ end
+
+ it "handles exceptions by logging them" do
+ allow(Gitlab::GitalyClient).to receive(:call).with(anything, :commit_service, :count_commits, anything, anything)
+ .and_raise(e = StandardError.new('_message_'))
+
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(e, experiment: 'new_project_readme')
+
+ subject.track_initial_writes(project)
+ end
+ end
+end
diff --git a/spec/experiments/strategy/round_robin_spec.rb b/spec/experiments/strategy/round_robin_spec.rb
new file mode 100644
index 00000000000..f837a4701b2
--- /dev/null
+++ b/spec/experiments/strategy/round_robin_spec.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Strategy::RoundRobin, :clean_gitlab_redis_shared_state do
+ subject(:round_robin) { described_class.new('_key_', %i[variant1 variant2]) }
+
+ describe "execute" do
+ context "when there are 2 variants" do
+ it "proves out round robin in selection", :aggregate_failures do
+ expect(round_robin.execute).to eq :variant2
+ expect(round_robin.execute).to eq :variant1
+ expect(round_robin.execute).to eq :variant2
+ end
+ end
+
+ context "when there are more than 2 variants" do
+ subject(:round_robin) { described_class.new('_key_', %i[variant1 variant2 variant3]) }
+
+ it "proves out round robin in selection", :aggregate_failures do
+ expect(round_robin.execute).to eq :variant2
+ expect(round_robin.execute).to eq :variant3
+ expect(round_robin.execute).to eq :variant1
+
+ expect(round_robin.execute).to eq :variant2
+ expect(round_robin.execute).to eq :variant3
+ expect(round_robin.execute).to eq :variant1
+ end
+ end
+
+ context "when writing to cache fails" do
+ subject(:round_robin) { described_class.new('_key_', []) }
+
+ it "raises an error and logs" do
+ allow(Gitlab::Redis::SharedState).to receive(:with).and_raise(Strategy::RoundRobin::CacheError)
+ expect(Gitlab::AppLogger).to receive(:warn)
+
+ expect { round_robin.execute }.to raise_error(Strategy::RoundRobin::CacheError)
+ end
+ end
+ end
+
+ describe "#counter_expires_in" do
+ it 'displays the expiration time in seconds' do
+ round_robin.execute
+
+ expect(round_robin.counter_expires_in).to be_between(0, described_class::COUNTER_EXPIRE_TIME)
+ end
+ end
+
+ describe '#value' do
+ it 'get the count' do
+ expect(round_robin.counter_value).to eq(0)
+
+ round_robin.execute
+
+ expect(round_robin.counter_value).to eq(1)
+ end
+ end
+
+ describe '#reset!' do
+ it 'resets the count down to zero' do
+ 3.times { round_robin.execute }
+
+ expect { round_robin.reset! }.to change { round_robin.counter_value }.from(3).to(0)
+ end
+ end
+end
diff --git a/spec/factories/audit_events.rb b/spec/factories/audit_events.rb
index 4e72976a9e5..05b86d2f13b 100644
--- a/spec/factories/audit_events.rb
+++ b/spec/factories/audit_events.rb
@@ -49,6 +49,21 @@ FactoryBot.define do
end
end
+ trait :unauthenticated do
+ author_id { -1 }
+ details do
+ {
+ custom_message: 'Custom action',
+ author_name: 'An unauthenticated user',
+ target_id: target_project.id,
+ target_type: 'Project',
+ target_details: target_project.name,
+ ip_address: '127.0.0.1',
+ entity_path: target_project.full_path
+ }
+ end
+ end
+
trait :group_event do
transient { target_group { association(:group) } }
diff --git a/spec/factories/ci/bridge.rb b/spec/factories/ci/bridge.rb
index 7727a468633..6cbcabca7ab 100644
--- a/spec/factories/ci/bridge.rb
+++ b/spec/factories/ci/bridge.rb
@@ -1,17 +1,10 @@
# frozen_string_literal: true
FactoryBot.define do
- factory :ci_bridge, class: 'Ci::Bridge' do
+ factory :ci_bridge, class: 'Ci::Bridge', parent: :ci_processable do
name { 'bridge' }
- stage { 'test' }
- stage_idx { 0 }
- ref { 'master' }
- tag { false }
created_at { '2013-10-29 09:50:00 CET' }
status { :created }
- scheduling_type { 'stage' }
-
- pipeline factory: :ci_pipeline
trait :variables do
yaml_variables do
@@ -53,6 +46,11 @@ FactoryBot.define do
finished_at { '2013-10-29 09:53:28 CET' }
end
+ trait :success do
+ finished
+ status { 'success' }
+ end
+
trait :failed do
finished
status { 'failed' }
@@ -75,5 +73,9 @@ FactoryBot.define do
trait :playable do
manual
end
+
+ trait :allowed_to_fail do
+ allow_failure { true }
+ end
end
end
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index 24abad66530..c4f9a4ce82b 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -3,15 +3,10 @@
include ActionDispatch::TestProcess
FactoryBot.define do
- factory :ci_build, class: 'Ci::Build' do
+ factory :ci_build, class: 'Ci::Build', parent: :ci_processable do
name { 'test' }
- stage { 'test' }
- stage_idx { 0 }
- ref { 'master' }
- tag { false }
add_attribute(:protected) { false }
created_at { 'Di 29. Okt 09:50:00 CET 2013' }
- scheduling_type { 'stage' }
pending
options do
@@ -28,7 +23,6 @@ FactoryBot.define do
]
end
- pipeline factory: :ci_pipeline
project { pipeline.project }
trait :degenerated do
@@ -79,10 +73,6 @@ FactoryBot.define do
status { 'created' }
end
- trait :waiting_for_resource do
- status { 'waiting_for_resource' }
- end
-
trait :preparing do
status { 'preparing' }
end
@@ -213,14 +203,6 @@ FactoryBot.define do
trigger_request factory: :ci_trigger_request
end
- trait :resource_group do
- waiting_for_resource_at { 5.minutes.ago }
-
- after(:build) do |build, evaluator|
- build.resource_group = create(:ci_resource_group, project: build.project)
- end
- end
-
trait :with_deployment do
after(:build) do |build, evaluator|
##
@@ -314,6 +296,18 @@ FactoryBot.define do
end
end
+ trait :sast_report do
+ after(:build) do |build|
+ build.job_artifacts << create(:ci_job_artifact, :sast, job: build)
+ end
+ end
+
+ trait :secret_detection_report do
+ after(:build) do |build|
+ build.job_artifacts << create(:ci_job_artifact, :secret_detection, job: build)
+ end
+ end
+
trait :test_reports do
after(:build) do |build|
build.job_artifacts << create(:ci_job_artifact, :junit, job: build)
diff --git a/spec/factories/ci/daily_build_group_report_results.rb b/spec/factories/ci/daily_build_group_report_results.rb
index d836ee9567c..55f4f116c97 100644
--- a/spec/factories/ci/daily_build_group_report_results.rb
+++ b/spec/factories/ci/daily_build_group_report_results.rb
@@ -7,6 +7,7 @@ FactoryBot.define do
project
last_pipeline factory: :ci_pipeline
group_name { 'rspec' }
+ group
data do
{ 'coverage' => 77.0 }
end
diff --git a/spec/factories/ci/job_artifacts.rb b/spec/factories/ci/job_artifacts.rb
index ad98e9d1f24..bfd8506566b 100644
--- a/spec/factories/ci/job_artifacts.rb
+++ b/spec/factories/ci/job_artifacts.rb
@@ -269,6 +269,26 @@ FactoryBot.define do
end
end
+ trait :sast do
+ file_type { :sast }
+ file_format { :raw }
+
+ after(:build) do |artifact, _|
+ artifact.file = fixture_file_upload(
+ Rails.root.join('spec/fixtures/security_reports/master/gl-sast-report.json'), 'application/json')
+ end
+ end
+
+ trait :secret_detection do
+ file_type { :secret_detection }
+ file_format { :raw }
+
+ after(:build) do |artifact, _|
+ artifact.file = fixture_file_upload(
+ Rails.root.join('spec/fixtures/security_reports/master/gl-secret-detection-report.json'), 'application/json')
+ end
+ end
+
trait :lsif do
file_type { :lsif }
file_format { :zip }
diff --git a/spec/factories/ci/pipeline_artifacts.rb b/spec/factories/ci/pipeline_artifacts.rb
index fa33609dd6c..05ff7afed7c 100644
--- a/spec/factories/ci/pipeline_artifacts.rb
+++ b/spec/factories/ci/pipeline_artifacts.rb
@@ -4,18 +4,30 @@ FactoryBot.define do
factory :ci_pipeline_artifact, class: 'Ci::PipelineArtifact' do
pipeline factory: :ci_pipeline
project { pipeline.project }
- file_type { :code_coverage }
file_format { :raw }
file_store { ObjectStorage::SUPPORTED_STORES.first }
- size { 1.megabytes }
-
+ size { 1.megabyte }
+ file_type { :code_coverage }
after(:build) do |artifact, _evaluator|
artifact.file = fixture_file_upload(
Rails.root.join('spec/fixtures/pipeline_artifacts/code_coverage.json'), 'application/json')
end
- trait :with_multibyte_characters do
+ trait :with_coverage_report do
+ file_type { :code_coverage }
+
+ after(:build) do |artifact, _evaluator|
+ artifact.file = fixture_file_upload(
+ Rails.root.join('spec/fixtures/pipeline_artifacts/code_coverage.json'), 'application/json')
+ end
+
+ size { file.size }
+ end
+
+ trait :with_coverage_multibyte_characters do
+ file_type { :code_coverage }
size { { "utf8" => "✓" }.to_json.bytesize }
+
after(:build) do |artifact, _evaluator|
artifact.file = CarrierWaveStringFile.new_file(
file_content: { "utf8" => "✓" }.to_json,
@@ -26,12 +38,25 @@ FactoryBot.define do
end
trait :with_code_coverage_with_multiple_files do
+ file_type { :code_coverage }
+
after(:build) do |artifact, _evaluator|
artifact.file = fixture_file_upload(
Rails.root.join('spec/fixtures/pipeline_artifacts/code_coverage_with_multiple_files.json'), 'application/json'
)
end
+ size { 1.megabyte }
+ end
+
+ trait :with_codequality_mr_diff_report do
+ file_type { :code_quality_mr_diff }
+
+ after(:build) do |artifact, _evaluator|
+ artifact.file = fixture_file_upload(
+ Rails.root.join('spec/fixtures/pipeline_artifacts/code_quality_mr_diff.json'), 'application/json')
+ end
+
size { file.size }
end
end
diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb
index 86a8b008e48..e0d7ad3c133 100644
--- a/spec/factories/ci/pipelines.rb
+++ b/spec/factories/ci/pipelines.rb
@@ -101,6 +101,22 @@ FactoryBot.define do
end
end
+ trait :with_sast_report do
+ status { :success }
+
+ after(:build) do |pipeline, evaluator|
+ pipeline.builds << build(:ci_build, :sast_report, pipeline: pipeline, project: pipeline.project)
+ end
+ end
+
+ trait :with_secret_detection_report do
+ status { :success }
+
+ after(:build) do |pipeline, evaluator|
+ pipeline.builds << build(:ci_build, :secret_detection_report, pipeline: pipeline, project: pipeline.project)
+ end
+ end
+
trait :with_test_reports do
status { :success }
@@ -159,7 +175,13 @@ FactoryBot.define do
trait :with_coverage_report_artifact do
after(:build) do |pipeline, evaluator|
- pipeline.pipeline_artifacts << build(:ci_pipeline_artifact, pipeline: pipeline, project: pipeline.project)
+ pipeline.pipeline_artifacts << build(:ci_pipeline_artifact, :with_coverage_report, pipeline: pipeline, project: pipeline.project)
+ end
+ end
+
+ trait :with_codequality_mr_diff_report do
+ after(:build) do |pipeline, evaluator|
+ pipeline.pipeline_artifacts << build(:ci_pipeline_artifact, :with_codequality_mr_diff_report, pipeline: pipeline, project: pipeline.project)
end
end
diff --git a/spec/factories/ci/processable.rb b/spec/factories/ci/processable.rb
new file mode 100644
index 00000000000..0550f4c23fa
--- /dev/null
+++ b/spec/factories/ci/processable.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :ci_processable, class: 'Ci::Processable' do
+ name { 'processable' }
+ stage { 'test' }
+ stage_idx { 0 }
+ ref { 'master' }
+ tag { false }
+ pipeline factory: :ci_pipeline
+ project { pipeline.project }
+ scheduling_type { 'stage' }
+
+ trait :waiting_for_resource do
+ status { 'waiting_for_resource' }
+ end
+
+ trait :resource_group do
+ waiting_for_resource_at { 5.minutes.ago }
+
+ after(:build) do |processable, evaluator|
+ processable.resource_group = create(:ci_resource_group, project: processable.project)
+ end
+ end
+ end
+end
diff --git a/spec/factories/ci/reports/codequality_degradations.rb b/spec/factories/ci/reports/codequality_degradations.rb
new file mode 100644
index 00000000000..d82157b457a
--- /dev/null
+++ b/spec/factories/ci/reports/codequality_degradations.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :codequality_degradation_1, class: Hash do
+ skip_create
+
+ initialize_with do
+ {
+ "categories": [
+ "Complexity"
+ ],
+ "check_name": "argument_count",
+ "content": {
+ "body": ""
+ },
+ "description": "Avoid parameter lists longer than 5 parameters. [12/5]",
+ "fingerprint": "15cdb5c53afd42bc22f8ca366a08d547",
+ "location": {
+ "path": "file_a.rb",
+ "lines": {
+ "begin": 10,
+ "end": 10
+ }
+ },
+ "other_locations": [],
+ "remediation_points": 900000,
+ "severity": "major",
+ "type": "issue",
+ "engine_name": "structure"
+ }.with_indifferent_access
+ end
+ end
+
+ factory :codequality_degradation_2, class: Hash do
+ skip_create
+
+ initialize_with do
+ {
+ "categories": [
+ "Complexity"
+ ],
+ "check_name": "argument_count",
+ "content": {
+ "body": ""
+ },
+ "description": "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.",
+ "fingerprint": "f3bdc1e8c102ba5fbd9e7f6cda51c95e",
+ "location": {
+ "path": "file_a.rb",
+ "lines": {
+ "begin": 10,
+ "end": 10
+ }
+ },
+ "other_locations": [],
+ "remediation_points": 900000,
+ "severity": "major",
+ "type": "issue",
+ "engine_name": "structure"
+ }.with_indifferent_access
+ end
+ end
+
+ factory :codequality_degradation_3, class: Hash do
+ skip_create
+
+ initialize_with do
+ {
+ "type": "Issue",
+ "check_name": "Rubocop/Metrics/ParameterLists",
+ "description": "Avoid parameter lists longer than 5 parameters. [12/5]",
+ "categories": [
+ "Complexity"
+ ],
+ "remediation_points": 550000,
+ "location": {
+ "path": "file_b.rb",
+ "positions": {
+ "begin": {
+ "column": 14,
+ "line": 10
+ },
+ "end": {
+ "column": 39,
+ "line": 10
+ }
+ }
+ },
+ "content": {
+ "body": "This cop checks for methods with too many parameters.\nThe maximum number of parameters is configurable.\nKeyword arguments can optionally be excluded from the total count."
+ },
+ "engine_name": "rubocop",
+ "fingerprint": "ab5f8b935886b942d621399f5a2ca16e",
+ "severity": "minor"
+ }.with_indifferent_access
+ end
+ end
+end
diff --git a/spec/factories/ci/resource.rb b/spec/factories/ci/resource.rb
index 515329506e5..dec26013a25 100644
--- a/spec/factories/ci/resource.rb
+++ b/spec/factories/ci/resource.rb
@@ -5,7 +5,7 @@ FactoryBot.define do
resource_group factory: :ci_resource_group
trait(:retained) do
- build factory: :ci_build
+ processable factory: :ci_build
end
end
end
diff --git a/spec/factories/design_management/design_at_version.rb b/spec/factories/design_management/design_at_version.rb
index 3d85269ee27..6c0848ed11f 100644
--- a/spec/factories/design_management/design_at_version.rb
+++ b/spec/factories/design_management/design_at_version.rb
@@ -17,7 +17,7 @@ FactoryBot.define do
attrs[:design] ||= association(:design, issue: issue)
attrs[:version] ||= association(:design_version, issue: issue)
- new(attrs)
+ new(**attrs)
end
end
end
diff --git a/spec/factories/diff_position.rb b/spec/factories/diff_position.rb
index 0185c4ce156..41f9a7b574e 100644
--- a/spec/factories/diff_position.rb
+++ b/spec/factories/diff_position.rb
@@ -24,7 +24,7 @@ FactoryBot.define do
head_sha { diff_refs&.head_sha }
start_sha { diff_refs&.start_sha }
- initialize_with { new(attributes) }
+ initialize_with { new(**attributes) }
trait :moved do
new_path { 'path/to/new.file' }
diff --git a/spec/factories/merge_request_diffs.rb b/spec/factories/merge_request_diffs.rb
index 481cabdae6d..f93f3f22109 100644
--- a/spec/factories/merge_request_diffs.rb
+++ b/spec/factories/merge_request_diffs.rb
@@ -10,12 +10,18 @@ FactoryBot.define do
head_commit_sha { Digest::SHA1.hexdigest(SecureRandom.hex) }
start_commit_sha { Digest::SHA1.hexdigest(SecureRandom.hex) }
+ diff_type { :regular }
+
trait :external do
external_diff { fixture_file_upload("spec/fixtures/doc_sample.txt", "plain/txt") }
stored_externally { true }
importing { true } # this avoids setting the state to 'empty'
end
+ trait :merge_head do
+ diff_type { :merge_head }
+ end
+
factory :external_merge_request_diff, traits: [:external]
end
end
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index e69743122cc..fce44c2cee0 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -21,7 +21,12 @@ FactoryBot.define do
merge_status { "can_be_merged" }
- trait :with_diffs do
+ trait :draft_merge_request do
+ title { generate(:draft_title) }
+ end
+
+ trait :wip_merge_request do
+ title { generate(:wip_title) }
end
trait :jira_title do
@@ -200,6 +205,18 @@ FactoryBot.define do
end
end
+ trait :with_codequality_mr_diff_reports do
+ after(:build) do |merge_request|
+ merge_request.head_pipeline = build(
+ :ci_pipeline,
+ :success,
+ :with_codequality_mr_diff_report,
+ project: merge_request.source_project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha)
+ end
+ end
+
trait :with_terraform_reports do
after(:build) do |merge_request|
merge_request.head_pipeline = build(
@@ -212,6 +229,30 @@ FactoryBot.define do
end
end
+ trait :with_sast_reports do
+ after(:build) do |merge_request|
+ merge_request.head_pipeline = build(
+ :ci_pipeline,
+ :success,
+ :with_sast_report,
+ project: merge_request.source_project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha)
+ end
+ end
+
+ trait :with_secret_detection_reports do
+ after(:build) do |merge_request|
+ merge_request.head_pipeline = build(
+ :ci_pipeline,
+ :success,
+ :with_secret_detection_report,
+ project: merge_request.source_project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha)
+ end
+ end
+
trait :with_exposed_artifacts do
after(:build) do |merge_request|
merge_request.head_pipeline = build(
@@ -294,7 +335,7 @@ FactoryBot.define do
factory :closed_merge_request, traits: [:closed]
factory :reopened_merge_request, traits: [:opened]
factory :invalid_merge_request, traits: [:invalid]
- factory :merge_request_with_diffs, traits: [:with_diffs]
+ factory :merge_request_with_diffs
factory :merge_request_with_diff_notes do
after(:create) do |mr|
create(:diff_note_on_merge_request, noteable: mr, project: mr.source_project)
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index 4b1f3194ce5..299d08972b7 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -8,7 +8,7 @@ FactoryBot.define do
factory :note do
project
note { generate(:title) }
- author { project&.creator || create(:user) }
+ author { project&.creator || association(:user) }
on_issue
factory :note_on_commit, traits: [:on_commit]
@@ -55,7 +55,7 @@ FactoryBot.define do
end
position do
- build(:text_diff_position,
+ association(:text_diff_position,
file: "files/ruby/popen.rb",
old_line: nil,
new_line: line_number,
@@ -64,7 +64,7 @@ FactoryBot.define do
trait :folded_position do
position do
- build(:text_diff_position,
+ association(:text_diff_position,
file: "files/ruby/popen.rb",
old_line: 1,
new_line: 1,
@@ -74,7 +74,7 @@ FactoryBot.define do
factory :image_diff_note_on_merge_request do
position do
- build(:image_diff_position,
+ association(:image_diff_position,
file: "files/images/any_image.png",
diff_refs: diff_refs)
end
@@ -90,7 +90,7 @@ FactoryBot.define do
end
position do
- build(:text_diff_position,
+ association(:text_diff_position,
file: "files/ruby/popen.rb",
old_line: nil,
new_line: line_number,
@@ -100,7 +100,11 @@ FactoryBot.define do
end
factory :diff_note_on_design, parent: :note, traits: [:on_design], class: 'DiffNote' do
- position { build(:image_diff_position, file: noteable.full_path, diff_refs: noteable.diff_refs) }
+ position do
+ association(:image_diff_position,
+ file: noteable.full_path,
+ diff_refs: noteable.diff_refs)
+ end
end
trait :on_commit do
diff --git a/spec/factories/packages.rb b/spec/factories/packages.rb
index 31f1aabe5dd..2c64abefb01 100644
--- a/spec/factories/packages.rb
+++ b/spec/factories/packages.rb
@@ -6,6 +6,15 @@ FactoryBot.define do
name { 'my/company/app/my-app' }
sequence(:version) { |n| "1.#{n}-SNAPSHOT" }
package_type { :maven }
+ status { :default }
+
+ trait :hidden do
+ status { :hidden }
+ end
+
+ trait :processing do
+ status { :processing }
+ end
factory :maven_package do
maven_metadatum
@@ -21,6 +30,23 @@ FactoryBot.define do
end
end
+ factory :rubygems_package do
+ sequence(:name) { |n| "my_gem_#{n}" }
+ sequence(:version) { |n| "1.#{n}" }
+ package_type { :rubygems }
+
+ after :create do |package|
+ create :package_file, :gem, package: package
+ create :package_file, :gemspec, package: package
+ end
+
+ trait(:with_metadatum) do
+ after :build do |pkg|
+ pkg.rubygems_metadatum = build(:rubygems_metadatum)
+ end
+ end
+ end
+
factory :debian_package do
sequence(:name) { |n| "package-#{n}" }
sequence(:version) { |n| "1.0-#{n}" }
@@ -29,6 +55,15 @@ FactoryBot.define do
transient do
without_package_files { false }
file_metadatum_trait { :keep }
+ published_in { :create }
+ end
+
+ after :build do |package, evaluator|
+ if evaluator.published_in == :create
+ create(:debian_publication, package: package)
+ elsif !evaluator.published_in.nil?
+ create(:debian_publication, package: package, distribution: evaluator.published_in)
+ end
end
after :create do |package, evaluator|
@@ -50,6 +85,7 @@ FactoryBot.define do
transient do
without_package_files { false }
file_metadatum_trait { :unknown }
+ published_in { nil }
end
end
end
@@ -176,6 +212,24 @@ FactoryBot.define do
composer_json { { name: 'foo' } }
end
+ factory :composer_cache_file, class: 'Packages::Composer::CacheFile' do
+ group
+
+ file_sha256 { '1' * 64 }
+
+ transient do
+ file_fixture { 'spec/fixtures/packages/composer/package.json' }
+ end
+
+ after(:build) do |cache_file, evaluator|
+ cache_file.file = fixture_file_upload(evaluator.file_fixture)
+ end
+
+ trait(:object_storage) do
+ file_store { Packages::Composer::CacheUploader::Store::REMOTE }
+ end
+ end
+
factory :maven_metadatum, class: 'Packages::Maven::Metadatum' do
association :package, package_type: :maven
path { 'my/company/app/my-app/1.0-SNAPSHOT' }
diff --git a/spec/factories/packages/debian/component_file.rb b/spec/factories/packages/debian/component_file.rb
new file mode 100644
index 00000000000..19157b3c8c6
--- /dev/null
+++ b/spec/factories/packages/debian/component_file.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :debian_project_component_file, class: 'Packages::Debian::ProjectComponentFile' do
+ component { association(:debian_project_component) }
+ architecture { association(:debian_project_architecture, distribution: component.distribution) }
+
+ factory :debian_group_component_file, class: 'Packages::Debian::GroupComponentFile' do
+ component { association(:debian_group_component) }
+ architecture { association(:debian_group_architecture, distribution: component.distribution) }
+ end
+
+ file_type { :packages }
+
+ after(:build) do |component_file, evaluator|
+ component_file.file = fixture_file_upload('spec/fixtures/packages/debian/distribution/Packages')
+ end
+
+ file_md5 { '12345abcde' }
+ file_sha256 { 'be93151dc23ac34a82752444556fe79b32c7a1ad' }
+
+ trait(:packages) do
+ file_type { :packages }
+ end
+
+ trait(:source) do
+ file_type { :source }
+ architecture { nil }
+ end
+
+ trait(:di_packages) do
+ file_type { :di_packages }
+ end
+
+ trait(:object_storage) do
+ file_store { Packages::PackageFileUploader::Store::REMOTE }
+ end
+ end
+end
diff --git a/spec/factories/packages/debian/distribution.rb b/spec/factories/packages/debian/distribution.rb
index 2015f2923b8..619308e4e18 100644
--- a/spec/factories/packages/debian/distribution.rb
+++ b/spec/factories/packages/debian/distribution.rb
@@ -14,7 +14,7 @@ FactoryBot.define do
trait(:with_file) do
after(:build) do |distribution, evaluator|
- distribution.file = fixture_file_upload('spec/fixtures/packages/debian/README.md')
+ distribution.file = fixture_file_upload('spec/fixtures/packages/debian/distribution/Release')
end
end
diff --git a/spec/factories/packages/debian/group_component.rb b/spec/factories/packages/debian/group_component.rb
new file mode 100644
index 00000000000..92d438be389
--- /dev/null
+++ b/spec/factories/packages/debian/group_component.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :debian_group_component, class: 'Packages::Debian::GroupComponent' do
+ distribution { association(:debian_group_distribution) }
+
+ sequence(:name) { |n| "group-component-#{n}" }
+ end
+end
diff --git a/spec/factories/packages/debian/project_component.rb b/spec/factories/packages/debian/project_component.rb
new file mode 100644
index 00000000000..a56aec4cef0
--- /dev/null
+++ b/spec/factories/packages/debian/project_component.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :debian_project_component, class: 'Packages::Debian::ProjectComponent' do
+ distribution { association(:debian_project_distribution) }
+
+ sequence(:name) { |n| "project-component-#{n}" }
+ end
+end
diff --git a/spec/factories/packages/debian/publication.rb b/spec/factories/packages/debian/publication.rb
new file mode 100644
index 00000000000..314c7064e9b
--- /dev/null
+++ b/spec/factories/packages/debian/publication.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :debian_publication, class: 'Packages::Debian::Publication' do
+ package { association(:debian_package, published_in: nil) }
+
+ distribution { association(:debian_project_distribution, project: package.project) }
+ end
+end
diff --git a/spec/factories/packages/package_file.rb b/spec/factories/packages/package_file.rb
index c328c01ec95..6d8b119040e 100644
--- a/spec/factories/packages/package_file.rb
+++ b/spec/factories/packages/package_file.rb
@@ -221,6 +221,22 @@ FactoryBot.define do
size { 300.kilobytes }
end
+ trait(:gem) do
+ package
+ file_fixture { 'spec/fixtures/packages/rubygems/package-0.0.1.gem' }
+ file_name { 'package-0.0.1.gem' }
+ file_sha1 { '5fe852b2a6abd96c22c11fa1ff2fb19d9ce58b57' }
+ size { 4.kilobytes }
+ end
+
+ trait(:gemspec) do
+ package
+ file_fixture { 'spec/fixtures/packages/rubygems/package.gemspec' }
+ file_name { 'package.gemspec' }
+ file_sha1 { '5fe852b2a6abd96c22c11fa1ff2fb19d9ce58b57' }
+ size { 242.bytes }
+ end
+
trait(:pypi) do
package
file_fixture { 'spec/fixtures/packages/pypi/sample-project.tar.gz' }
diff --git a/spec/factories/packages/rubygems/metadata.rb b/spec/factories/packages/rubygems/metadata.rb
new file mode 100644
index 00000000000..9f03bf80dc3
--- /dev/null
+++ b/spec/factories/packages/rubygems/metadata.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :rubygems_metadatum, class: 'Packages::Rubygems::Metadatum' do
+ package { association(:rubygems_package) }
+ authors { FFaker::Name.name }
+ email { FFaker::Internet.email }
+ end
+end
diff --git a/spec/factories/pages_deployments.rb b/spec/factories/pages_deployments.rb
index 56aab4fa9f3..d3e2fefb4ae 100644
--- a/spec/factories/pages_deployments.rb
+++ b/spec/factories/pages_deployments.rb
@@ -4,12 +4,20 @@ FactoryBot.define do
factory :pages_deployment, class: 'PagesDeployment' do
project
- after(:build) do |deployment, _evaluator|
- filepath = Rails.root.join("spec/fixtures/pages.zip")
+ transient do
+ filename { nil }
+ end
+
+ trait(:migrated) do
+ filename { PagesDeployment::MIGRATED_FILE_NAME }
+ end
+
+ after(:build) do |deployment, evaluator|
+ file = UploadedFile.new("spec/fixtures/pages.zip", filename: evaluator.filename)
- deployment.file = fixture_file_upload(filepath)
- deployment.file_sha256 = Digest::SHA256.file(filepath).hexdigest
- ::Zip::File.open(filepath) do |zip_archive|
+ deployment.file = file
+ deployment.file_sha256 = Digest::SHA256.file(file.path).hexdigest
+ ::Zip::File.open(file.path) do |zip_archive|
deployment.file_count = zip_archive.count
end
end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 54a5dea49bb..e8e0362fc62 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -40,7 +40,7 @@ FactoryBot.define do
group_runners_enabled { nil }
merge_pipelines_enabled { nil }
merge_trains_enabled { nil }
- ci_keep_latest_artifact { nil }
+ keep_latest_artifact { nil }
import_status { nil }
import_jid { nil }
import_correlation_id { nil }
@@ -86,7 +86,7 @@ FactoryBot.define do
project.group_runners_enabled = evaluator.group_runners_enabled unless evaluator.group_runners_enabled.nil?
project.merge_pipelines_enabled = evaluator.merge_pipelines_enabled unless evaluator.merge_pipelines_enabled.nil?
project.merge_trains_enabled = evaluator.merge_trains_enabled unless evaluator.merge_trains_enabled.nil?
- project.ci_keep_latest_artifact = evaluator.ci_keep_latest_artifact unless evaluator.ci_keep_latest_artifact.nil?
+ project.keep_latest_artifact = evaluator.keep_latest_artifact unless evaluator.keep_latest_artifact.nil?
project.restrict_user_defined_variables = evaluator.restrict_user_defined_variables unless evaluator.restrict_user_defined_variables.nil?
if evaluator.import_status
diff --git a/spec/factories/sequences.rb b/spec/factories/sequences.rb
index b338fd99625..f9952cd9966 100644
--- a/spec/factories/sequences.rb
+++ b/spec/factories/sequences.rb
@@ -15,6 +15,8 @@ FactoryBot.define do
sequence(:sha) { |n| Digest::SHA1.hexdigest("commit-like-#{n}") }
sequence(:oid) { |n| Digest::SHA2.hexdigest("oid-like-#{n}") }
sequence(:variable) { |n| "var#{n}" }
+ sequence(:draft_title) { |n| "Draft: #{n}" }
+ sequence(:wip_title) { |n| "WIP: #{n}" }
sequence(:jira_title) { |n| "[PROJ-#{n}]: fix bug" }
sequence(:jira_branch) { |n| "feature/PROJ-#{n}" }
end
diff --git a/spec/factories/services_data.rb b/spec/factories/services_data.rb
index c62fff2af55..7b6a705c791 100644
--- a/spec/factories/services_data.rb
+++ b/spec/factories/services_data.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
-# these factories should never be called directly, they are used when creating services
+# These factories should not be called directly unless we are testing a _tracker_data model.
+# The factories are used when creating integrations.
FactoryBot.define do
factory :jira_tracker_data do
service
diff --git a/spec/factories/token_with_ivs.rb b/spec/factories/token_with_ivs.rb
new file mode 100644
index 00000000000..68989f6c5bc
--- /dev/null
+++ b/spec/factories/token_with_ivs.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :token_with_iv do
+ hashed_token { ::Digest::SHA256.digest(SecureRandom.hex(50)) }
+ iv { ::Digest::SHA256.digest(SecureRandom.hex(50)) }
+ hashed_plaintext_token { ::Digest::SHA256.digest(SecureRandom.hex(50)) }
+ end
+end
diff --git a/spec/factories/u2f_registrations.rb b/spec/factories/u2f_registrations.rb
index 7017b0ee9e7..40ad221415c 100644
--- a/spec/factories/u2f_registrations.rb
+++ b/spec/factories/u2f_registrations.rb
@@ -2,6 +2,8 @@
FactoryBot.define do
factory :u2f_registration do
+ user
+
certificate { FFaker::BaconIpsum.characters(728) }
key_handle { FFaker::BaconIpsum.characters(86) }
public_key { FFaker::BaconIpsum.characters(88) }
diff --git a/spec/factories/usage_data.rb b/spec/factories/usage_data.rb
index 0ed4176109a..714f8451f39 100644
--- a/spec/factories/usage_data.rb
+++ b/spec/factories/usage_data.rb
@@ -121,8 +121,8 @@ FactoryBot.define do
env = create(:environment, project: projects[3])
[3, 31].each do |n|
deployment_options = { created_at: n.days.ago, project: env.project, environment: env }
- create(:deployment, :failed, deployment_options)
- create(:deployment, :success, deployment_options)
+ create(:deployment, :failed, **deployment_options)
+ create(:deployment, :success, **deployment_options)
create_list(:project_snippet, 2, project: projects[0], created_at: n.days.ago)
create(:personal_snippet, created_at: n.days.ago)
end
diff --git a/spec/features/admin/admin_cohorts_spec.rb b/spec/features/admin/admin_cohorts_spec.rb
deleted file mode 100644
index 982a9333275..00000000000
--- a/spec/features/admin/admin_cohorts_spec.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'Cohorts page' do
- before do
- admin = create(:admin)
- sign_in(admin)
- gitlab_enable_admin_mode_sign_in(admin)
- end
-
- context 'with usage ping enabled' do
- it 'shows users count per month' do
- stub_application_setting(usage_ping_enabled: true)
-
- create_list(:user, 2)
-
- visit admin_cohorts_path
-
- expect(page).to have_content("#{Time.now.strftime('%b %Y')} 3 0")
- end
- end
-
- context 'with usage ping disabled' do
- it 'shows empty state', :js do
- stub_application_setting(usage_ping_enabled: false)
-
- visit admin_cohorts_path
-
- expect(page).to have_selector(".js-empty-state")
- end
- end
-end
diff --git a/spec/features/admin/admin_disables_git_access_protocol_spec.rb b/spec/features/admin/admin_disables_git_access_protocol_spec.rb
index f7f0592a315..b370b779afe 100644
--- a/spec/features/admin/admin_disables_git_access_protocol_spec.rb
+++ b/spec/features/admin/admin_disables_git_access_protocol_spec.rb
@@ -37,7 +37,10 @@ RSpec.describe 'Admin disables Git access protocol', :js do
it 'shows only the SSH clone information' do
resize_screen_xs
visit_project
- find('.dropdown-toggle').click
+
+ within('.js-mobile-git-clone') do
+ find('.dropdown-toggle').click
+ end
expect(page).to have_content('Copy SSH clone URL')
expect(page).not_to have_content('Copy HTTP clone URL')
@@ -66,7 +69,10 @@ RSpec.describe 'Admin disables Git access protocol', :js do
it 'shows only the HTTP clone information' do
resize_screen_xs
visit_project
- find('.dropdown-toggle').click
+
+ within('.js-mobile-git-clone') do
+ find('.dropdown-toggle').click
+ end
expect(page).to have_content('Copy HTTP clone URL')
expect(page).not_to have_content('Copy SSH clone URL')
@@ -97,7 +103,10 @@ RSpec.describe 'Admin disables Git access protocol', :js do
it 'shows both SSH and HTTP clone information' do
resize_screen_xs
visit_project
- find('.dropdown-toggle').click
+
+ within('.js-mobile-git-clone') do
+ find('.dropdown-toggle').click
+ end
expect(page).to have_content('Copy HTTP clone URL')
expect(page).to have_content('Copy SSH clone URL')
diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb
index a8e18385bd2..bbdf2f7f4a9 100644
--- a/spec/features/admin/admin_groups_spec.rb
+++ b/spec/features/admin/admin_groups_spec.rb
@@ -54,7 +54,7 @@ RSpec.describe 'Admin Groups' do
click_button "Create group"
expect(current_path).to eq admin_group_path(Group.find_by(path: path_component))
- content = page.find('div#content-body')
+ content = page.find('#content-body')
h3_texts = content.all('h3').collect(&:text).join("\n")
expect(h3_texts).to match group_name
li_texts = content.all('li').collect(&:text).join("\n")
diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb
index ff4e592234b..aab2e6d7cef 100644
--- a/spec/features/admin/admin_projects_spec.rb
+++ b/spec/features/admin/admin_projects_spec.rb
@@ -3,6 +3,7 @@
require 'spec_helper'
RSpec.describe "Admin::Projects" do
+ include Spec::Support::Helpers::Features::MembersHelpers
include Select2Helper
let(:user) { create :user }
@@ -91,45 +92,97 @@ RSpec.describe "Admin::Projects" do
end
end
- describe 'add admin himself to a project' do
- before do
- project.add_maintainer(user)
- end
+ context 'when `vue_project_members_list` feature flag is enabled', :js do
+ describe 'admin adds themselves to the project' do
+ before do
+ project.add_maintainer(user)
+ stub_feature_flags(invite_members_group_modal: false)
+ end
- it 'adds admin a to a project as developer', :js do
- visit project_project_members_path(project)
+ it 'adds admin to the project as developer', :js do
+ visit project_project_members_path(project)
- page.within '.invite-users-form' do
- select2(current_user.id, from: '#user_ids', multiple: true)
- select 'Developer', from: 'access_level'
+ page.within '.invite-users-form' do
+ select2(current_user.id, from: '#user_ids', multiple: true)
+ select 'Developer', from: 'access_level'
+ end
+
+ click_button 'Invite'
+
+ expect(find_member_row(current_user)).to have_content('Developer')
end
+ end
+
+ describe 'admin removes themselves from the project' do
+ before do
+ project.add_maintainer(user)
+ project.add_developer(current_user)
+ end
+
+ it 'removes admin from the project' do
+ visit project_project_members_path(project)
+
+ expect(find_member_row(current_user)).to have_content('Developer')
+
+ page.within find_member_row(current_user) do
+ click_button 'Leave'
+ end
- click_button 'Invite'
+ page.within('[role="dialog"]') do
+ click_button('Leave')
+ end
- page.within '.content-list' do
- expect(page).to have_content(current_user.name)
- expect(page).to have_content('Developer')
+ expect(current_path).to match dashboard_projects_path
end
end
end
- describe 'admin remove himself from a project' do
+ context 'when `vue_project_members_list` feature flag is disabled' do
before do
- project.add_maintainer(user)
- project.add_developer(current_user)
+ stub_feature_flags(vue_project_members_list: false)
end
- it 'removes admin from the project' do
- visit project_project_members_path(project)
+ describe 'admin adds themselves to the project' do
+ before do
+ project.add_maintainer(user)
+ stub_feature_flags(invite_members_group_modal: false)
+ end
+
+ it 'adds admin to the project as developer', :js do
+ visit project_project_members_path(project)
+
+ page.within '.invite-users-form' do
+ select2(current_user.id, from: '#user_ids', multiple: true)
+ select 'Developer', from: 'access_level'
+ end
+
+ click_button 'Invite'
+
+ page.within '.content-list' do
+ expect(page).to have_content(current_user.name)
+ expect(page).to have_content('Developer')
+ end
+ end
+ end
- page.within '.content-list' do
- expect(page).to have_content(current_user.name)
- expect(page).to have_content('Developer')
+ describe 'admin removes themselves from the project' do
+ before do
+ project.add_maintainer(user)
+ project.add_developer(current_user)
end
- find(:css, '.content-list li', text: current_user.name).find(:css, 'a.btn-danger').click
+ it 'removes admin from the project' do
+ visit project_project_members_path(project)
+
+ page.within '.content-list' do
+ expect(page).to have_content(current_user.name)
+ expect(page).to have_content('Developer')
+ end
- expect(page).not_to have_selector(:css, '.content-list')
+ find(:css, '.content-list li', text: current_user.name).find(:css, 'a.btn-danger').click
+
+ expect(page).not_to have_selector(:css, '.content-list')
+ end
end
end
end
diff --git a/spec/features/admin/admin_search_settings_spec.rb b/spec/features/admin/admin_search_settings_spec.rb
new file mode 100644
index 00000000000..a78d17a6651
--- /dev/null
+++ b/spec/features/admin/admin_search_settings_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Admin searches application settings', :js do
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:application_settings) { create(:application_setting) }
+
+ before do
+ sign_in(admin)
+ gitlab_enable_admin_mode_sign_in(admin)
+ end
+
+ context 'in appearances page' do
+ before do
+ visit(admin_appearances_path)
+ end
+
+ it_behaves_like 'cannot search settings'
+ end
+
+ context 'in ci/cd settings page' do
+ let(:visit_path) { ci_cd_admin_application_settings_path }
+
+ it_behaves_like 'can search settings with feature flag check', 'Variables', 'Package Registry'
+ end
+end
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index 0c66775c323..52f39f65bd0 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -306,59 +306,70 @@ RSpec.describe 'Admin updates settings' do
page.within('.as-ci-cd') do
check 'Default to Auto DevOps pipeline for all projects'
fill_in 'application_setting_auto_devops_domain', with: 'domain.com'
+ uncheck 'Keep the latest artifacts for all jobs in the latest successful pipelines'
click_button 'Save changes'
end
expect(current_settings.auto_devops_enabled?).to be true
expect(current_settings.auto_devops_domain).to eq('domain.com')
+ expect(current_settings.keep_latest_artifact).to be false
expect(page).to have_content "Application settings saved successfully"
end
context 'Container Registry' do
- context 'delete tags service execution timeout' do
- let(:feature_flag_enabled) { true }
- let(:client_support) { true }
-
- before do
- stub_container_registry_config(enabled: true)
- stub_feature_flags(container_registry_expiration_policies_throttling: feature_flag_enabled)
- allow(ContainerRegistry::Client).to receive(:supports_tag_delete?).and_return(client_support)
- end
+ let(:feature_flag_enabled) { true }
+ let(:client_support) { true }
+ let(:settings_titles) do
+ {
+ container_registry_delete_tags_service_timeout: 'Container Registry delete tags service execution timeout',
+ container_registry_expiration_policies_worker_capacity: 'Cleanup policy maximum workers running concurrently',
+ container_registry_cleanup_tags_service_max_list_size: 'Cleanup policy maximum number of tags to be deleted'
+ }
+ end
+
+ before do
+ stub_container_registry_config(enabled: true)
+ stub_feature_flags(container_registry_expiration_policies_throttling: feature_flag_enabled)
+ allow(ContainerRegistry::Client).to receive(:supports_tag_delete?).and_return(client_support)
+ end
- RSpec.shared_examples 'not having service timeout settings' do
- it 'lacks the timeout settings' do
- visit ci_cd_admin_application_settings_path
+ shared_examples 'not having container registry setting' do |registry_setting|
+ it "lacks the container setting #{registry_setting}" do
+ visit ci_cd_admin_application_settings_path
- expect(page).not_to have_content "Container Registry delete tags service execution timeout"
- end
+ expect(page).not_to have_content(settings_titles[registry_setting])
end
+ end
- context 'with feature flag enabled' do
- context 'with client supporting tag delete' do
- it 'changes the timeout' do
- visit ci_cd_admin_application_settings_path
+ %i[container_registry_delete_tags_service_timeout container_registry_expiration_policies_worker_capacity container_registry_cleanup_tags_service_max_list_size].each do |setting|
+ context "for container registry setting #{setting}" do
+ context 'with feature flag enabled' do
+ context 'with client supporting tag delete' do
+ it 'changes the setting' do
+ visit ci_cd_admin_application_settings_path
- page.within('.as-registry') do
- fill_in 'application_setting_container_registry_delete_tags_service_timeout', with: 400
- click_button 'Save changes'
- end
+ page.within('.as-registry') do
+ fill_in "application_setting_#{setting}", with: 400
+ click_button 'Save changes'
+ end
- expect(current_settings.container_registry_delete_tags_service_timeout).to eq(400)
- expect(page).to have_content "Application settings saved successfully"
+ expect(current_settings.public_send(setting)).to eq(400)
+ expect(page).to have_content "Application settings saved successfully"
+ end
end
- end
- context 'with client not supporting tag delete' do
- let(:client_support) { false }
+ context 'with client not supporting tag delete' do
+ let(:client_support) { false }
- it_behaves_like 'not having service timeout settings'
+ it_behaves_like 'not having container registry setting', setting
+ end
end
- end
- context 'with feature flag disabled' do
- let(:feature_flag_enabled) { false }
+ context 'with feature flag disabled' do
+ let(:feature_flag_enabled) { false }
- it_behaves_like 'not having service timeout settings'
+ it_behaves_like 'not having container registry setting', setting
+ end
end
end
end
diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb
new file mode 100644
index 00000000000..4fc60d17886
--- /dev/null
+++ b/spec/features/admin/admin_users_spec.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe "Admin::Users" do
+ let(:current_user) { create(:admin) }
+
+ before do
+ sign_in(current_user)
+ gitlab_enable_admin_mode_sign_in(current_user)
+ end
+
+ describe 'Tabs', :js do
+ let(:tabs_selector) { '.js-users-tabs' }
+ let(:active_tab_selector) { '.nav-link.active' }
+
+ it 'does not add the tab param when the Users tab is selected' do
+ visit admin_users_path
+
+ within tabs_selector do
+ click_link 'Users'
+ end
+
+ expect(page).to have_current_path(admin_users_path)
+ end
+
+ it 'adds the ?tab=cohorts param when the Cohorts tab is selected' do
+ visit admin_users_path
+
+ within tabs_selector do
+ click_link 'Cohorts'
+ end
+
+ expect(page).to have_current_path(admin_users_path(tab: 'cohorts'))
+ end
+
+ it 'shows the cohorts tab when the tab param is set' do
+ visit admin_users_path(tab: 'cohorts')
+
+ within tabs_selector do
+ expect(page).to have_selector active_tab_selector, text: 'Cohorts'
+ end
+ end
+ end
+
+ describe 'Cohorts tab content' do
+ context 'with usage ping enabled' do
+ it 'shows users count per month' do
+ stub_application_setting(usage_ping_enabled: true)
+
+ create_list(:user, 2)
+
+ visit admin_users_path(tab: 'cohorts')
+
+ expect(page).to have_content("#{Time.now.strftime('%b %Y')} 3 0")
+ end
+ end
+
+ context 'with usage ping disabled' do
+ it 'shows empty state', :js do
+ stub_application_setting(usage_ping_enabled: false)
+
+ visit admin_users_path(tab: 'cohorts')
+
+ expect(page).to have_selector(".js-empty-state")
+ expect(page).to have_content("Activate user activity analysis")
+ end
+ end
+ end
+end
diff --git a/spec/features/admin/users/user_spec.rb b/spec/features/admin/users/user_spec.rb
index e7dd50ed514..befa7bd338b 100644
--- a/spec/features/admin/users/user_spec.rb
+++ b/spec/features/admin/users/user_spec.rb
@@ -171,7 +171,7 @@ RSpec.describe 'Admin::Users::User' do
it 'logs in as the user when impersonate is clicked' do
subject
- expect(page.find(:css, '.header-user .profile-link')['data-user']).to eql(another_user.username)
+ expect(page.find(:css, '[data-testid="user-profile-link"]')['data-user']).to eql(another_user.username)
end
it 'sees impersonation log out icon' do
@@ -205,7 +205,7 @@ RSpec.describe 'Admin::Users::User' do
it 'logs out of impersonated user back to original user' do
subject
- expect(page.find(:css, '.header-user .profile-link')['data-user']).to eq(current_user.username)
+ expect(page.find(:css, '[data-testid="user-profile-link"]')['data-user']).to eq(current_user.username)
end
it 'is redirected back to the impersonated users page in the admin after stopping' do
diff --git a/spec/features/alert_management/alert_details_spec.rb b/spec/features/alert_management/alert_details_spec.rb
index d190e4b6939..ce82b5adf8d 100644
--- a/spec/features/alert_management/alert_details_spec.rb
+++ b/spec/features/alert_management/alert_details_spec.rb
@@ -47,7 +47,7 @@ RSpec.describe 'Alert details', :js do
expect(page).to have_selector('[data-testid="alert-todo-button"]')
todo_button = find('[data-testid="alert-todo-button"]')
- expect(todo_button).to have_content('Add a To-Do')
+ expect(todo_button).to have_content('Add a to do')
find('[data-testid="alert-todo-button"]').click
wait_for_requests
diff --git a/spec/features/alerts_settings/user_views_alerts_settings_spec.rb b/spec/features/alerts_settings/user_views_alerts_settings_spec.rb
index 698a36d3f76..07c87f98eb6 100644
--- a/spec/features/alerts_settings/user_views_alerts_settings_spec.rb
+++ b/spec/features/alerts_settings/user_views_alerts_settings_spec.rb
@@ -19,13 +19,14 @@ RSpec.describe 'Alert integrations settings form', :js do
describe 'when viewing alert integrations as a maintainer' do
context 'with the default page permissions' do
before do
+ stub_feature_flags(multiple_http_integrations_custom_mapping: false)
visit project_settings_operations_path(project, anchor: 'js-alert-management-settings')
wait_for_requests
end
it 'shows the alerts setting form title' do
page.within('#js-alert-management-settings') do
- expect(find('h3')).to have_content('Alerts')
+ expect(find('h4')).to have_content('Alerts')
end
end
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index b3cc2eb418d..2d6b669f28b 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -13,12 +13,14 @@ RSpec.describe 'Issue Boards', :js do
let_it_be(:user2) { create(:user) }
before do
+ stub_feature_flags(board_new_list: false)
+
project.add_maintainer(user)
project.add_maintainer(user2)
- set_cookie('sidebar_collapsed', 'true')
-
sign_in(user)
+
+ set_cookie('sidebar_collapsed', 'true')
end
context 'no lists' do
@@ -573,7 +575,7 @@ RSpec.describe 'Issue Boards', :js do
end
it 'shows the button' do
- expect(page).to have_link('Toggle focus mode')
+ expect(page).to have_button('Toggle focus mode')
end
end
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index 2af5b787a78..08bc70d7116 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -107,17 +107,20 @@ RSpec.describe 'Issue Boards', :js do
click_card(card)
page.within('.assignee') do
- click_link 'Edit'
+ click_button('Edit')
wait_for_requests
- page.within('.dropdown-menu-user') do
- click_link user.name
+ assignee = first('.gl-avatar-labeled').find('.gl-avatar-labeled-label').text
- wait_for_requests
+ page.within('.dropdown-menu-user') do
+ first('.gl-avatar-labeled').click
end
- expect(page).to have_content(user.name)
+ click_button('Edit')
+ wait_for_requests
+
+ expect(page).to have_content(assignee)
end
expect(card).to have_selector('.avatar')
@@ -128,15 +131,15 @@ RSpec.describe 'Issue Boards', :js do
click_card(card_two)
page.within('.assignee') do
- click_link 'Edit'
+ click_button('Edit')
wait_for_requests
page.within('.dropdown-menu-user') do
- click_link 'Unassigned'
+ find('[data-testid="unassign"]').click
end
- close_dropdown_menu_if_visible
+ click_button('Edit')
wait_for_requests
expect(page).to have_content('None')
@@ -165,17 +168,20 @@ RSpec.describe 'Issue Boards', :js do
click_card(card)
page.within('.assignee') do
- click_link 'Edit'
+ click_button('Edit')
wait_for_requests
- page.within('.dropdown-menu-user') do
- click_link user.name
+ assignee = first('.gl-avatar-labeled').find('.gl-avatar-labeled-label').text
- wait_for_requests
+ page.within('.dropdown-menu-user') do
+ first('.gl-avatar-labeled').click
end
- expect(page).to have_content(user.name)
+ click_button('Edit')
+ wait_for_requests
+
+ expect(page).to have_content(assignee)
end
page.within(find('.board:nth-child(2)')) do
@@ -183,9 +189,9 @@ RSpec.describe 'Issue Boards', :js do
end
page.within('.assignee') do
- click_link 'Edit'
+ click_button('Edit')
- expect(find('.dropdown-menu')).to have_selector('.is-active')
+ expect(find('.dropdown-menu')).to have_selector('.gl-new-dropdown-item-check-icon')
end
end
end
@@ -411,10 +417,10 @@ RSpec.describe 'Issue Boards', :js do
wait_for_requests
page.within('.subscriptions') do
- find('.js-issuable-subscribe-button button:not(.is-checked)').click
+ find('[data-testid="subscription-toggle"] button:not(.is-checked)').click
wait_for_requests
- expect(page).to have_css('.js-issuable-subscribe-button button.is-checked')
+ expect(page).to have_css('[data-testid="subscription-toggle"] button.is-checked')
end
end
@@ -427,10 +433,10 @@ RSpec.describe 'Issue Boards', :js do
wait_for_requests
page.within('.subscriptions') do
- find('.js-issuable-subscribe-button button.is-checked').click
+ find('[data-testid="subscription-toggle"] button.is-checked').click
wait_for_requests
- expect(page).to have_css('.js-issuable-subscribe-button button:not(.is-checked)')
+ expect(page).to have_css('[data-testid="subscription-toggle"] button:not(.is-checked)')
end
end
end
diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb
index 60d485d4558..ee156bdcab4 100644
--- a/spec/features/calendar_spec.rb
+++ b/spec/features/calendar_spec.rb
@@ -113,8 +113,8 @@ RSpec.describe 'Contributions Calendar', :js do
describe 'deselect calendar day' do
before do
cells[0].click
- page.find('.js-overview-tab a').click
wait_for_requests
+ cells[0].click
end
it 'hides calendar day activities' do
diff --git a/spec/features/commit_spec.rb b/spec/features/commit_spec.rb
new file mode 100644
index 00000000000..02754cc803e
--- /dev/null
+++ b/spec/features/commit_spec.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Commit' do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+
+ describe "single commit view" do
+ let(:commit) do
+ project.repository.commits(nil, limit: 100).find do |commit|
+ commit.diffs.size > 1
+ end
+ end
+
+ let(:files) { commit.diffs.diff_files.to_a }
+
+ before do
+ stub_feature_flags(async_commit_diff_files: false)
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ describe "commit details" do
+ before do
+ visit project_commit_path(project, commit)
+ end
+
+ it "shows the short commit message" do
+ expect(page).to have_content(commit.title)
+ end
+
+ it "reports the correct number of total changes" do
+ expect(page).to have_content("Changes #{commit.diffs.size}")
+ end
+ end
+
+ context "pagination enabled" do
+ before do
+ stub_feature_flags(paginate_commit_view: true)
+ stub_const("Projects::CommitController::COMMIT_DIFFS_PER_PAGE", 1)
+
+ visit project_commit_path(project, commit)
+ end
+
+ it "shows an adjusted count for changed files on this page" do
+ expect(page).to have_content("Showing 1 changed file")
+ end
+
+ it "shows only the first diff on the first page" do
+ expect(page).to have_selector(".files ##{files[0].file_hash}")
+ expect(page).not_to have_selector(".files ##{files[1].file_hash}")
+ end
+
+ it "can navigate to the second page" do
+ within(".files .gl-pagination") do
+ click_on("2")
+ end
+
+ expect(page).not_to have_selector(".files ##{files[0].file_hash}")
+ expect(page).to have_selector(".files ##{files[1].file_hash}")
+ end
+ end
+
+ context "pagination disabled" do
+ before do
+ stub_feature_flags(paginate_commit_view: false)
+
+ visit project_commit_path(project, commit)
+ end
+
+ it "shows both diffs on the page" do
+ expect(page).to have_selector(".files ##{files[0].file_hash}")
+ expect(page).to have_selector(".files ##{files[1].file_hash}")
+ end
+ end
+ end
+end
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index f8e84043c1b..1622979812d 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -138,9 +138,8 @@ RSpec.describe 'Commits' do
end
end
- context 'when accessing internal project with disallowed access', :js do
+ context 'when accessing internal project with disallowed access', :js, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/299575' do
before do
- stub_feature_flags(graphql_pipeline_header: false)
project.update(
visibility_level: Gitlab::VisibilityLevel::INTERNAL,
public_builds: false)
diff --git a/spec/features/dashboard/activity_spec.rb b/spec/features/dashboard/activity_spec.rb
index b419a063858..e75e661b513 100644
--- a/spec/features/dashboard/activity_spec.rb
+++ b/spec/features/dashboard/activity_spec.rb
@@ -9,6 +9,26 @@ RSpec.describe 'Dashboard > Activity' do
sign_in(user)
end
+ context 'tabs' do
+ it 'shows Your Projects' do
+ visit activity_dashboard_path
+
+ expect(find('.top-area .nav-tabs li.active')).to have_content('Your projects')
+ end
+
+ it 'shows Starred Projects' do
+ visit activity_dashboard_path(filter: 'starred')
+
+ expect(find('.top-area .nav-tabs li.active')).to have_content('Starred projects')
+ end
+
+ it 'shows Followed Projects' do
+ visit activity_dashboard_path(filter: 'followed')
+
+ expect(find('.top-area .nav-tabs li.active')).to have_content('Followed users')
+ end
+ end
+
context 'rss' do
before do
visit activity_dashboard_path
diff --git a/spec/features/discussion_comments/issue_spec.rb b/spec/features/discussion_comments/issue_spec.rb
index 2ad77a2884c..86743e31fbd 100644
--- a/spec/features/discussion_comments/issue_spec.rb
+++ b/spec/features/discussion_comments/issue_spec.rb
@@ -8,6 +8,8 @@ RSpec.describe 'Thread Comments Issue', :js do
let(:issue) { create(:issue, project: project) }
before do
+ stub_feature_flags(remove_comment_close_reopen: false)
+
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/discussion_comments/merge_request_spec.rb b/spec/features/discussion_comments/merge_request_spec.rb
index 761cc7ae796..82dcdf9f918 100644
--- a/spec/features/discussion_comments/merge_request_spec.rb
+++ b/spec/features/discussion_comments/merge_request_spec.rb
@@ -9,6 +9,7 @@ RSpec.describe 'Thread Comments Merge Request', :js do
before do
stub_feature_flags(remove_resolve_note: false)
+ stub_feature_flags(remove_comment_close_reopen: false)
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/discussion_comments/snippets_spec.rb b/spec/features/discussion_comments/snippets_spec.rb
index b2d3fbf4b5d..42053e571e9 100644
--- a/spec/features/discussion_comments/snippets_spec.rb
+++ b/spec/features/discussion_comments/snippets_spec.rb
@@ -4,15 +4,34 @@ require 'spec_helper'
RSpec.describe 'Thread Comments Snippet', :js do
let_it_be(:user) { create(:user) }
- let_it_be(:project) { create(:project) }
- let_it_be(:snippet) { create(:project_snippet, :private, :repository, project: project, author: user) }
before do
- project.add_maintainer(user)
sign_in(user)
+ end
+
+ context 'with project snippets' do
+ let_it_be(:project) do
+ create(:project).tap do |p|
+ p.add_maintainer(user)
+ end
+ end
+
+ let_it_be(:snippet) { create(:project_snippet, :private, :repository, project: project, author: user) }
+
+ before do
+ visit project_snippet_path(project, snippet)
+ end
- visit project_snippet_path(project, snippet)
+ it_behaves_like 'thread comments', 'snippet'
end
- it_behaves_like 'thread comments', 'snippet'
+ context 'with personal snippets' do
+ let_it_be(:snippet) { create(:personal_snippet, :private, :repository, author: user) }
+
+ before do
+ visit snippet_path(snippet)
+ end
+
+ it_behaves_like 'thread comments', 'snippet'
+ end
end
diff --git a/spec/features/groups/import_export/connect_instance_spec.rb b/spec/features/groups/import_export/connect_instance_spec.rb
index 2e1bf27ba8b..73de49101ea 100644
--- a/spec/features/groups/import_export/connect_instance_spec.rb
+++ b/spec/features/groups/import_export/connect_instance_spec.rb
@@ -23,8 +23,8 @@ RSpec.describe 'Import/Export - Connect to another instance', :js do
source_url = 'https://gitlab.com'
pat = 'demo-pat'
stub_path = 'stub-group'
-
- stub_request(:get, "%{url}/api/v4/groups?page=1&per_page=30&top_level_only=true&min_access_level=40" % { url: source_url }).to_return(
+ total = 37
+ stub_request(:get, "%{url}/api/v4/groups?page=1&per_page=20&top_level_only=true&min_access_level=40&search=" % { url: source_url }).to_return(
body: [{
id: 2595438,
web_url: 'https://gitlab.com/groups/auto-breakfast',
@@ -33,17 +33,25 @@ RSpec.describe 'Import/Export - Connect to another instance', :js do
full_name: 'Stub',
full_path: stub_path
}].to_json,
- headers: { 'Content-Type' => 'application/json' }
+ headers: {
+ 'Content-Type' => 'application/json',
+ 'X-Next-Page' => 2,
+ 'X-Page' => 1,
+ 'X-Per-Page' => 20,
+ 'X-Total' => total,
+ 'X-Total-Pages' => 2
+ }
)
expect(page).to have_content 'Import groups from another instance of GitLab'
+ expect(page).to have_content 'Not all related objects are migrated'
fill_in :bulk_import_gitlab_url, with: source_url
fill_in :bulk_import_gitlab_access_token, with: pat
click_on 'Connect instance'
- expect(page).to have_content 'Importing groups from %{url}' % { url: source_url }
+ expect(page).to have_content 'Showing 1-1 of %{total} groups from %{url}' % { url: source_url, total: total }
expect(page).to have_content stub_path
end
end
diff --git a/spec/features/groups/navbar_spec.rb b/spec/features/groups/navbar_spec.rb
index a4c450c9a2c..7025874a4ff 100644
--- a/spec/features/groups/navbar_spec.rb
+++ b/spec/features/groups/navbar_spec.rb
@@ -87,12 +87,4 @@ RSpec.describe 'Group navbar' do
it_behaves_like 'verified navigation bar'
end
-
- context 'when invite team members is not available' do
- it 'does not display the js-invite-members-trigger' do
- visit group_path(group)
-
- expect(page).not_to have_selector('.js-invite-members-trigger')
- end
- end
end
diff --git a/spec/features/groups/settings/packages_and_registries_spec.rb b/spec/features/groups/settings/packages_and_registries_spec.rb
index b8ffd73335d..45ea77e3868 100644
--- a/spec/features/groups/settings/packages_and_registries_spec.rb
+++ b/spec/features/groups/settings/packages_and_registries_spec.rb
@@ -7,15 +7,17 @@ RSpec.describe 'Group Packages & Registries settings' do
let(:user) { create(:user) }
let(:group) { create(:group) }
+ let(:sub_group) { create(:group, parent: group) }
before do
group.add_owner(user)
+ sub_group.add_owner(user)
sign_in(user)
end
- context 'when the feature flag is off' do
+ context 'when packges feature is disabled on the group' do
before do
- stub_feature_flags(packages_and_registries_group_settings: false)
+ stub_packages_setting(enabled: false)
end
it 'the menu item is not visible' do
@@ -25,9 +27,15 @@ RSpec.describe 'Group Packages & Registries settings' do
expect(settings_menu).not_to have_content 'Packages & Registries'
end
+
+ it 'renders 404 when navigating to page' do
+ visit_settings_page
+
+ expect(page).to have_content('Not Found')
+ end
end
- context 'when the feature flag is on' do
+ context 'when packages feature is enabled on the group' do
it 'the menu item is visible' do
visit group_path(group)
@@ -47,6 +55,56 @@ RSpec.describe 'Group Packages & Registries settings' do
sidebar = find('.nav-sidebar')
expect(sidebar).to have_link _('Packages & Registries')
end
+
+ it 'has a Package Registry section', :js do
+ visit_settings_page
+
+ expect(page).to have_content('Package Registry')
+ expect(page).to have_button('Collapse')
+ end
+
+ it 'automatically saves changes to the server', :js do
+ visit_settings_page
+
+ expect(page).to have_content('Allow duplicates')
+
+ find('.gl-toggle').click
+
+ expect(page).to have_content('Do not allow duplicates')
+
+ visit_settings_page
+
+ expect(page).to have_content('Do not allow duplicates')
+ end
+
+ it 'shows an error on wrong regex', :js do
+ visit_settings_page
+
+ expect(page).to have_content('Allow duplicates')
+
+ find('.gl-toggle').click
+
+ expect(page).to have_content('Do not allow duplicates')
+
+ fill_in 'Exceptions', with: ')'
+
+ # simulate blur event
+ find('body').click
+
+ expect(page).to have_content('is an invalid regexp')
+ end
+
+ context 'in a sub group' do
+ it 'works correctly', :js do
+ visit_sub_group_settings_page
+
+ expect(page).to have_content('Allow duplicates')
+
+ find('.gl-toggle').click
+
+ expect(page).to have_content('Do not allow duplicates')
+ end
+ end
end
def find_settings_menu
@@ -56,4 +114,8 @@ RSpec.describe 'Group Packages & Registries settings' do
def visit_settings_page
visit group_settings_packages_and_registries_path(group)
end
+
+ def visit_sub_group_settings_page
+ visit group_settings_packages_and_registries_path(sub_group)
+ end
end
diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb
index 3a42fd508b4..5067f11be67 100644
--- a/spec/features/groups/show_spec.rb
+++ b/spec/features/groups/show_spec.rb
@@ -163,6 +163,7 @@ RSpec.describe 'Group show page' do
let!(:project) { create(:project, namespace: group) }
before do
+ stub_feature_flags(vue_notification_dropdown: false)
group.add_maintainer(maintainer)
sign_in(maintainer)
end
diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb
index 1f8397e45f7..90647305281 100644
--- a/spec/features/help_pages_spec.rb
+++ b/spec/features/help_pages_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe 'Help Pages' do
it 'opens shortcuts help dialog' do
find('.js-trigger-shortcut').click
- expect(page).to have_selector('#modal-shortcuts')
+ expect(page).to have_selector('[data-testid="modal-shortcuts"]')
end
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 e6101e90a83..7ae43f35901 100644
--- a/spec/features/ide/user_opens_merge_request_spec.rb
+++ b/spec/features/ide/user_opens_merge_request_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'IDE merge request', :js do
- let(:merge_request) { create(:merge_request, :with_diffs, :simple, source_project: project) }
+ let(:merge_request) { create(:merge_request, :simple, source_project: project) }
let(:project) { create(:project, :public, :repository) }
let(:user) { project.owner }
diff --git a/spec/features/import/manifest_import_spec.rb b/spec/features/import/manifest_import_spec.rb
index cfd0c7e210f..dfd6211a683 100644
--- a/spec/features/import/manifest_import_spec.rb
+++ b/spec/features/import/manifest_import_spec.rb
@@ -52,6 +52,6 @@ RSpec.describe 'Import multiple repositories by uploading a manifest file', :js
end
def second_row
- page.all('table.import-table tbody tr')[1]
+ page.all('table tbody tr')[1]
end
end
diff --git a/spec/features/issuables/issuable_list_spec.rb b/spec/features/issuables/issuable_list_spec.rb
index 3f00bdc478d..a0786d36fdf 100644
--- a/spec/features/issuables/issuable_list_spec.rb
+++ b/spec/features/issuables/issuable_list_spec.rb
@@ -53,7 +53,7 @@ RSpec.describe 'issuable list', :js do
visit_issuable_list(:issue)
- expect(page).to have_text('Open ? Closed ? All ?')
+ expect(page).to have_text('Open Closed All')
end
it "counts merge requests closing issues icons for each issue" do
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index 9b2a11c4b0e..e2087868035 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -183,6 +183,16 @@ RSpec.describe 'GFM autocomplete', :js do
expect(find('#at-view-users')).to have_content(user.name)
end
+ it 'searches across full name for assignees' do
+ page.within '.timeline-content-form' do
+ find('#note-body').native.send_keys('@speciąlsome')
+ end
+
+ wait_for_requests
+
+ expect(find('.atwho-view li', visible: true)).to have_content(user.name)
+ end
+
it 'selects the first item for non-assignee dropdowns if a query is entered' do
page.within '.timeline-content-form' do
find('#note-body').native.send_keys(':1')
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index 59fba5f65e0..ca44978d223 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -11,6 +11,11 @@ RSpec.describe 'Issue Sidebar' do
let!(:label) { create(:label, project: project, title: 'bug') }
let(:issue) { create(:labeled_issue, project: project, labels: [label]) }
let!(:xss_label) { create(:label, project: project, title: '&lt;script&gt;alert("xss");&lt;&#x2F;script&gt;') }
+ let!(:milestone_expired) { create(:milestone, project: project, due_date: 5.days.ago) }
+ let!(:milestone_no_duedate) { create(:milestone, project: project, title: 'Foo - No due date') }
+ let!(:milestone1) { create(:milestone, project: project, title: 'Milestone-1', due_date: 20.days.from_now) }
+ let!(:milestone2) { create(:milestone, project: project, title: 'Milestone-2', due_date: 15.days.from_now) }
+ let!(:milestone3) { create(:milestone, project: project, title: 'Milestone-3', due_date: 10.days.from_now) }
before do
stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab")
@@ -134,6 +139,36 @@ RSpec.describe 'Issue Sidebar' do
end
end
+ context 'editing issue milestone', :js do
+ before do
+ page.within('.block.milestone > .title') do
+ click_on 'Edit'
+ end
+ end
+
+ it 'shows milestons list in the dropdown' do
+ page.within('.block.milestone .dropdown-content') do
+ # 5 milestones + "No milestone" = 6 items
+ expect(page.find('ul')).to have_selector('li[data-milestone-id]', count: 6)
+ end
+ end
+
+ it 'shows expired milestone at the bottom of the list' do
+ page.within('.block.milestone .dropdown-content ul') do
+ expect(page.find('li:last-child')).to have_content milestone_expired.title
+ end
+ end
+
+ it 'shows milestone due earliest at the top of the list' do
+ page.within('.block.milestone .dropdown-content ul') do
+ expect(page.all('li[data-milestone-id]')[1]).to have_content milestone3.title
+ expect(page.all('li[data-milestone-id]')[2]).to have_content milestone2.title
+ expect(page.all('li[data-milestone-id]')[3]).to have_content milestone1.title
+ expect(page.all('li[data-milestone-id]')[4]).to have_content milestone_no_duedate.title
+ end
+ end
+ end
+
context 'editing issue labels', :js do
before do
issue.update(labels: [label])
diff --git a/spec/features/issues/issue_state_spec.rb b/spec/features/issues/issue_state_spec.rb
index d5a115433aa..409f498798b 100644
--- a/spec/features/issues/issue_state_spec.rb
+++ b/spec/features/issues/issue_state_spec.rb
@@ -42,9 +42,15 @@ RSpec.describe 'issue state', :js do
end
describe 'when open', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/297348' do
- context 'when clicking the top `Close issue` button', :aggregate_failures do
- let(:open_issue) { create(:issue, project: project) }
+ let(:open_issue) { create(:issue, project: project) }
+ it_behaves_like 'page with comment and close button', 'Close issue' do
+ def setup
+ visit project_issue_path(project, open_issue)
+ end
+ end
+
+ context 'when clicking the top `Close issue` button', :aggregate_failures do
before do
visit project_issue_path(project, open_issue)
end
@@ -53,9 +59,8 @@ RSpec.describe 'issue state', :js do
end
context 'when clicking the bottom `Close issue` button', :aggregate_failures do
- let(:open_issue) { create(:issue, project: project) }
-
before do
+ stub_feature_flags(remove_comment_close_reopen: false)
visit project_issue_path(project, open_issue)
end
@@ -64,9 +69,15 @@ RSpec.describe 'issue state', :js do
end
describe 'when closed', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/297201' do
- context 'when clicking the top `Reopen issue` button', :aggregate_failures do
- let(:closed_issue) { create(:issue, project: project, state: 'closed') }
+ let(:closed_issue) { create(:issue, project: project, state: 'closed') }
+ it_behaves_like 'page with comment and close button', 'Reopen issue' do
+ def setup
+ visit project_issue_path(project, closed_issue)
+ end
+ end
+
+ context 'when clicking the top `Reopen issue` button', :aggregate_failures do
before do
visit project_issue_path(project, closed_issue)
end
@@ -75,9 +86,8 @@ RSpec.describe 'issue state', :js do
end
context 'when clicking the bottom `Reopen issue` button', :aggregate_failures do
- let(:closed_issue) { create(:issue, project: project, state: 'closed') }
-
before do
+ stub_feature_flags(remove_comment_close_reopen: false)
visit project_issue_path(project, closed_issue)
end
diff --git a/spec/features/issues/user_comments_on_issue_spec.rb b/spec/features/issues/user_comments_on_issue_spec.rb
index 24a261f592b..004488f2f64 100644
--- a/spec/features/issues/user_comments_on_issue_spec.rb
+++ b/spec/features/issues/user_comments_on_issue_spec.rb
@@ -39,8 +39,6 @@ RSpec.describe "User comments on issue", :js do
add_note(comment)
- wait_for_requests
-
expect(page.find('pre code').text).to eq code_block_content
end
@@ -51,8 +49,6 @@ RSpec.describe "User comments on issue", :js do
add_note(comment)
- wait_for_requests
-
expect(page.find('svg.mermaid')).to have_content html_content
within('svg.mermaid') { expect(page).not_to have_selector('img') }
end
diff --git a/spec/features/issues/user_creates_issue_by_email_spec.rb b/spec/features/issues/user_creates_issue_by_email_spec.rb
index 5a0036170ab..c47f24ab836 100644
--- a/spec/features/issues/user_creates_issue_by_email_spec.rb
+++ b/spec/features/issues/user_creates_issue_by_email_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe 'Issues > User creates issue by email' do
project.add_developer(user)
end
- describe 'new issue by email' do
+ describe 'new issue by email', :js do
shared_examples 'show the email in the modal' do
let(:issue) { create(:issue, project: project) }
@@ -28,7 +28,7 @@ RSpec.describe 'Issues > User creates issue by email' do
page.within '#issuable-email-modal' do
email = project.new_issuable_address(user, 'issue')
- expect(page).to have_selector("input[value='#{email}']")
+ expect(page.find('input[type="text"]').value).to eq email
end
end
end
diff --git a/spec/features/issues/user_resets_their_incoming_email_token_spec.rb b/spec/features/issues/user_resets_their_incoming_email_token_spec.rb
index a20f65abebf..2b1c25174c2 100644
--- a/spec/features/issues/user_resets_their_incoming_email_token_spec.rb
+++ b/spec/features/issues/user_resets_their_incoming_email_token_spec.rb
@@ -16,17 +16,17 @@ RSpec.describe 'Issues > User resets their incoming email token' do
end
it 'changes incoming email address token', :js do
- find('.issuable-email-modal-btn').click
- previous_token = find('input#issuable_email').value
- find('.incoming-email-token-reset').click
-
- wait_for_requests
-
- expect(page).to have_no_field('issuable_email', with: previous_token)
- new_token = project.new_issuable_address(user.reload, 'issue')
- expect(page).to have_field(
- 'issuable_email',
- with: new_token
- )
+ page.find('[data-testid="issuable-email-modal-btn"]').click
+
+ page.within '#issuable-email-modal' do
+ previous_token = page.find('input[type="text"]').value
+ page.find('[data-testid="incoming-email-token-reset"]').click
+
+ wait_for_requests
+
+ expect(page.find('input[type="text"]').value).not_to eq previous_token
+ new_token = project.new_issuable_address(user.reload, 'issue')
+ expect(page.find('input[type="text"]').value).to eq new_token
+ end
end
end
diff --git a/spec/features/issues/user_toggles_subscription_spec.rb b/spec/features/issues/user_toggles_subscription_spec.rb
index 971c8a3b431..d91c187c840 100644
--- a/spec/features/issues/user_toggles_subscription_spec.rb
+++ b/spec/features/issues/user_toggles_subscription_spec.rb
@@ -15,13 +15,13 @@ RSpec.describe "User toggles subscription", :js do
end
it "unsubscribes from issue" do
- subscription_button = find(".js-issuable-subscribe-button")
+ subscription_button = find('[data-testid="subscription-toggle"]')
# Check we're subscribed.
expect(subscription_button).to have_css("button.is-checked")
# Toggle subscription.
- find(".js-issuable-subscribe-button button").click
+ find('[data-testid="subscription-toggle"]').click
wait_for_requests
# Check we're unsubscribed.
@@ -33,7 +33,7 @@ RSpec.describe "User toggles subscription", :js do
it 'is disabled' do
expect(page).to have_content('Notifications have been disabled by the project or group owner')
- expect(page).not_to have_selector('.js-issuable-subscribe-button')
+ expect(page).not_to have_selector('[data-testid="subscription-toggle"]')
end
end
end
diff --git a/spec/features/labels_hierarchy_spec.rb b/spec/features/labels_hierarchy_spec.rb
index 5d141580874..aeb42cc2edb 100644
--- a/spec/features/labels_hierarchy_spec.rb
+++ b/spec/features/labels_hierarchy_spec.rb
@@ -18,6 +18,7 @@ RSpec.describe 'Labels Hierarchy', :js do
before do
stub_feature_flags(graphql_board_lists: false)
+ stub_feature_flags(board_new_list: false)
grandparent.add_owner(user)
sign_in(user)
@@ -270,6 +271,10 @@ RSpec.describe 'Labels Hierarchy', :js do
end
context 'creating boards lists' do
+ before do
+ stub_feature_flags(board_new_list: false)
+ end
+
context 'on project boards' do
let(:board) { create(:board, project: project_1) }
diff --git a/spec/features/markdown/markdown_spec.rb b/spec/features/markdown/markdown_spec.rb
index 151ef76e884..8e28f89f49e 100644
--- a/spec/features/markdown/markdown_spec.rb
+++ b/spec/features/markdown/markdown_spec.rb
@@ -206,6 +206,9 @@ RSpec.describe 'GitLab Markdown', :aggregate_failures do
# `markdown` helper expects a `@project` and `@group` variable
@project = @feat.project
@group = @feat.group
+
+ stub_application_setting(plantuml_enabled: true, plantuml_url: 'http://localhost:8080')
+ stub_application_setting(kroki_enabled: true, kroki_url: 'http://localhost:8000')
end
let(:project) { @feat.project } # Shadow this so matchers can use it
@@ -265,6 +268,18 @@ RSpec.describe 'GitLab Markdown', :aggregate_failures do
aggregate_failures 'ColorFilter' do
expect(doc).to parse_colors
end
+
+ aggregate_failures 'MermaidFilter' do
+ expect(doc).to parse_mermaid
+ end
+
+ aggregate_failures 'PlantumlFilter' do
+ expect(doc).to parse_plantuml
+ end
+
+ aggregate_failures 'KrokiFilter' do
+ expect(doc).to parse_kroki
+ end
end
end
@@ -338,6 +353,18 @@ RSpec.describe 'GitLab Markdown', :aggregate_failures do
aggregate_failures 'ColorFilter' do
expect(doc).to parse_colors
end
+
+ aggregate_failures 'MermaidFilter' do
+ expect(doc).to parse_mermaid
+ end
+
+ aggregate_failures 'PlantumlFilter' do
+ expect(doc).to parse_plantuml
+ end
+
+ aggregate_failures 'KrokiFilter' do
+ expect(doc).to parse_kroki
+ end
end
end
diff --git a/spec/features/markdown/mermaid_spec.rb b/spec/features/markdown/mermaid_spec.rb
index 23cdd9d2ce5..207678e07c3 100644
--- a/spec/features/markdown/mermaid_spec.rb
+++ b/spec/features/markdown/mermaid_spec.rb
@@ -108,7 +108,7 @@ RSpec.describe 'Mermaid rendering', :js do
expect(svg[:style]).to match(/max-width/)
expect(svg[:width].to_i).to eq(100)
- expect(svg[:height].to_i).to eq(0)
+ expect(svg[:height].to_i).to be_within(5).of(220)
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 3d18aef9327..d4b185a82e9 100644
--- a/spec/features/merge_request/user_accepts_merge_request_spec.rb
+++ b/spec/features/merge_request/user_accepts_merge_request_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inline do
- let(:merge_request) { create(:merge_request, :with_diffs, :simple, source_project: project) }
+ let(:merge_request) { create(:merge_request, :simple, source_project: project) }
let(:project) { create(:project, :public, :repository) }
let(:user) { create(:user) }
diff --git a/spec/features/merge_request/user_closes_reopens_merge_request_state_spec.rb b/spec/features/merge_request/user_closes_reopens_merge_request_state_spec.rb
index 2b94c072c8b..ab3ef7c1ac0 100644
--- a/spec/features/merge_request/user_closes_reopens_merge_request_state_spec.rb
+++ b/spec/features/merge_request/user_closes_reopens_merge_request_state_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User closes/reopens a merge request', :js do
+RSpec.describe 'User closes/reopens a merge request', :js, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/297500' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
@@ -12,9 +12,15 @@ RSpec.describe 'User closes/reopens a merge request', :js do
end
describe 'when open' do
- context 'when clicking the top `Close merge request` link', :aggregate_failures do
- let(:open_merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:open_merge_request) { create(:merge_request, source_project: project, target_project: project) }
+
+ it_behaves_like 'page with comment and close button', 'Close merge request' do
+ def setup
+ visit merge_request_path(open_merge_request)
+ end
+ end
+ context 'when clicking the top `Close merge request` link', :aggregate_failures do
before do
visit merge_request_path(open_merge_request)
end
@@ -34,9 +40,8 @@ RSpec.describe 'User closes/reopens a merge request', :js do
end
context 'when clicking the bottom `Close merge request` button', :aggregate_failures do
- let(:open_merge_request) { create(:merge_request, source_project: project, target_project: project) }
-
before do
+ stub_feature_flags(remove_comment_close_reopen: false)
visit merge_request_path(open_merge_request)
end
@@ -55,10 +60,23 @@ RSpec.describe 'User closes/reopens a merge request', :js do
end
end
- describe 'when closed', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/297500' do
- context 'when clicking the top `Reopen merge request` link', :aggregate_failures do
- let(:closed_merge_request) { create(:merge_request, source_project: project, target_project: project, state: 'closed') }
+ describe 'when closed' do
+ let(:closed_merge_request) { create(:merge_request, source_project: project, target_project: project, state: 'closed') }
+ it_behaves_like 'page with comment and close button', 'Close merge request' do
+ def setup
+ visit merge_request_path(closed_merge_request)
+
+ within '.detail-page-header' do
+ click_button 'Toggle dropdown'
+ click_link 'Reopen merge request'
+ end
+
+ wait_for_requests
+ end
+ end
+
+ context 'when clicking the top `Reopen merge request` link', :aggregate_failures do
before do
visit merge_request_path(closed_merge_request)
end
@@ -78,9 +96,8 @@ RSpec.describe 'User closes/reopens a merge request', :js do
end
context 'when clicking the bottom `Reopen merge request` button', :aggregate_failures do
- let(:closed_merge_request) { create(:merge_request, source_project: project, target_project: project, state: 'closed') }
-
before do
+ stub_feature_flags(remove_comment_close_reopen: false)
visit merge_request_path(closed_merge_request)
end
diff --git a/spec/features/merge_request/user_edits_mr_spec.rb b/spec/features/merge_request/user_edits_mr_spec.rb
index 817b4e0b48e..2c949ed84f4 100644
--- a/spec/features/merge_request/user_edits_mr_spec.rb
+++ b/spec/features/merge_request/user_edits_mr_spec.rb
@@ -20,14 +20,4 @@ RSpec.describe 'Merge request > User edits MR' do
include_context 'merge request edit context'
it_behaves_like 'an editable merge request'
end
-
- context 'when merge_request_reviewers is turned off' do
- before do
- stub_feature_flags(merge_request_reviewers: false)
- end
-
- it 'does not render reviewers dropdown' do
- expect(page).not_to have_selector('.js-reviewer-search')
- end
- end
end
diff --git a/spec/features/merge_request/user_manages_subscription_spec.rb b/spec/features/merge_request/user_manages_subscription_spec.rb
index 9ed5b67fa0e..3cdb22000f6 100644
--- a/spec/features/merge_request/user_manages_subscription_spec.rb
+++ b/spec/features/merge_request/user_manages_subscription_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe 'User manages subscription', :js do
end
it 'toggles subscription' do
- page.within('.js-issuable-subscribe-button') do
+ page.within('[data-testid="subscription-toggle"]') do
wait_for_requests
expect(page).to have_css 'button:not(.is-checked)'
diff --git a/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb b/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb
index ea3e90a4508..8438c0af553 100644
--- a/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb
+++ b/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb
@@ -57,7 +57,7 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js do
wait_for_requests
expect(page).to have_css('button[disabled="disabled"]', text: 'Merge')
- expect(page).to have_content('Please retry the job or push a new commit to fix the failure')
+ expect(page).to have_content('The pipeline for this merge request did not complete. Push a new commit to fix the failure, or check the troubleshooting documentation to see other possible actions.')
end
end
@@ -70,7 +70,7 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js do
wait_for_requests
expect(page).not_to have_button 'Merge'
- expect(page).to have_content('Please retry the job or push a new commit to fix the failure')
+ expect(page).to have_content('The pipeline for this merge request did not complete. Push a new commit to fix the failure, or check the troubleshooting documentation to see other possible actions.')
end
end
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 5e99383e4a1..63b463a2c5f 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
@@ -68,7 +68,7 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do
wait_for_requests
- expect(page).to have_content 'Merge when pipeline succeeds', wait: 0
+ expect(page).to have_content 'Merge when pipeline succeeds'
end
it_behaves_like 'Merge when pipeline succeeds activator'
@@ -145,7 +145,7 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do
before do
merge_request.update!(
merge_user: merge_request.author,
- merge_error: 'Something went wrong.'
+ merge_error: 'Something went wrong'
)
refresh
end
@@ -155,7 +155,7 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do
wait_for_requests
page.within('.mr-section-container') do
- expect(page).to have_content('Merge failed: Something went wrong. Please try again.')
+ expect(page).to have_content('Something went wrong. Try again.')
end
end
end
@@ -174,7 +174,7 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do
wait_for_requests
page.within('.mr-section-container') do
- expect(page).to have_content('Merge failed: Something went wrong. Please try again.')
+ expect(page).to have_content('Something went wrong. Try again.')
end
end
end
diff --git a/spec/features/merge_request/user_posts_notes_spec.rb b/spec/features/merge_request/user_posts_notes_spec.rb
index 489582521b5..e629bc0dc53 100644
--- a/spec/features/merge_request/user_posts_notes_spec.rb
+++ b/spec/features/merge_request/user_posts_notes_spec.rb
@@ -161,7 +161,7 @@ RSpec.describe 'Merge request > User posts notes', :js do
fill_in 'note[note]', with: 'Some new content'
accept_confirm do
- find('.btn-cancel').click
+ find('[data-testid="cancel"]').click
end
end
expect(find('.js-note-text').text).to eq ''
diff --git a/spec/features/merge_request/user_reverts_merge_request_spec.rb b/spec/features/merge_request/user_reverts_merge_request_spec.rb
index 5e9611de460..9cbba6c470f 100644
--- a/spec/features/merge_request/user_reverts_merge_request_spec.rb
+++ b/spec/features/merge_request/user_reverts_merge_request_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'User reverts a merge request', :js do
- let(:merge_request) { create(:merge_request, :with_diffs, :simple, source_project: project) }
+ let(:merge_request) { create(:merge_request, :simple, source_project: project) }
let(:project) { create(:project, :public, :repository) }
let(:user) { create(:user) }
@@ -17,46 +17,28 @@ RSpec.describe 'User reverts a merge request', :js do
wait_for_requests
- visit(merge_request_path(merge_request))
+ # do not reload the page by visiting, let javascript update the page as it will validate we have loaded the modal
+ # code correctly on page update that adds the `revert` button
end
it 'reverts a merge request', :sidekiq_might_not_need_inline do
- find("a[href='#modal-revert-commit']").click
+ revert_commit
- page.within('#modal-revert-commit') do
- uncheck('create_merge_request')
- click_button('Revert')
- end
+ wait_for_requests
expect(page).to have_content('The merge request has been successfully reverted.')
-
- wait_for_requests
end
it 'does not revert a merge request that was previously reverted', :sidekiq_might_not_need_inline do
- find("a[href='#modal-revert-commit']").click
-
- page.within('#modal-revert-commit') do
- uncheck('create_merge_request')
- click_button('Revert')
- end
-
- find("a[href='#modal-revert-commit']").click
+ revert_commit
- page.within('#modal-revert-commit') do
- uncheck('create_merge_request')
- click_button('Revert')
- end
+ revert_commit
expect(page).to have_content('Sorry, we cannot revert this merge request automatically.')
end
it 'reverts a merge request in a new merge request', :sidekiq_might_not_need_inline do
- find("a[href='#modal-revert-commit']").click
-
- page.within('#modal-revert-commit') do
- click_button('Revert')
- end
+ revert_commit(create_merge_request: true)
expect(page).to have_content('The merge request has been successfully reverted. You can now submit a merge request to get this change into the original branch.')
end
@@ -68,4 +50,13 @@ RSpec.describe 'User reverts a merge request', :js do
expect(page).not_to have_link('Revert')
end
+
+ def revert_commit(create_merge_request: false)
+ click_button('Revert')
+
+ page.within('[data-testid="modal-commit"]') do
+ uncheck('create_merge_request') unless create_merge_request
+ click_button('Revert')
+ end
+ end
end
diff --git a/spec/features/merge_request/user_sees_cherry_pick_modal_spec.rb b/spec/features/merge_request/user_sees_cherry_pick_modal_spec.rb
index 7f4249336fe..78c1b2a718e 100644
--- a/spec/features/merge_request/user_sees_cherry_pick_modal_spec.rb
+++ b/spec/features/merge_request/user_sees_cherry_pick_modal_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe 'Merge request > User cherry-picks', :js do
it 'does not show a Cherry-pick button' do
visit project_merge_request_path(project, merge_request)
- expect(page).not_to have_link 'Cherry-pick'
+ expect(page).not_to have_button 'Cherry-pick'
end
end
@@ -40,7 +40,7 @@ RSpec.describe 'Merge request > User cherry-picks', :js do
it 'shows a Cherry-pick button' do
visit project_merge_request_path(project, merge_request)
- expect(page).to have_link 'Cherry-pick'
+ expect(page).to have_button 'Cherry-pick'
end
it 'hides the cherry pick button for an archived project' do
@@ -48,7 +48,7 @@ RSpec.describe 'Merge request > User cherry-picks', :js do
visit project_merge_request_path(project, merge_request)
- expect(page).not_to have_link 'Cherry-pick'
+ expect(page).not_to have_button 'Cherry-pick'
end
end
@@ -56,18 +56,12 @@ RSpec.describe 'Merge request > User cherry-picks', :js do
before do
visit project_merge_request_path(project, merge_request)
- click_link('Cherry-pick')
+ click_button('Cherry-pick')
end
it 'shows the cherry-pick modal' do
expect(page).to have_content('Cherry-pick this merge request')
end
-
- it 'closes the cherry-pick modal with escape keypress' do
- find('#modal-cherry-pick-commit').send_keys(:escape)
-
- expect(page).not_to have_content('Start a new merge request with these changes')
- end
end
end
end
diff --git a/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb b/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
index d9743f6f330..708ce53b4fe 100644
--- a/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
@@ -160,7 +160,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'merges the merge request' do
expect(page).to have_content('Merged by')
- expect(page).to have_link('Revert')
+ expect(page).to have_button('Revert')
end
end
@@ -357,7 +357,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'merges the merge request' do
expect(page).to have_content('Merged by')
- expect(page).to have_link('Revert')
+ expect(page).to have_button('Revert')
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 c2b2ada47be..0854a8b9fb7 100644
--- a/spec/features/merge_request/user_sees_merge_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb
@@ -77,15 +77,26 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
end
it 'allows me to merge, see cherry-pick modal and load branches list', :sidekiq_might_not_need_inline do
+ modal_selector = '[data-testid="modal-commit"]'
+
wait_for_requests
click_button 'Merge'
wait_for_requests
- click_link 'Cherry-pick'
- page.find('.js-project-refs-dropdown').click
- wait_for_requests
- expect(page.all('.js-cherry-pick-form .dropdown-content li').size).to be > 1
+ click_button 'Cherry-pick'
+
+ page.within(modal_selector) do
+ click_button 'master'
+ end
+
+ page.within("#{modal_selector} .dropdown-menu") do
+ find('[data-testid="dropdown-search-box"]').set('')
+
+ wait_for_requests
+
+ expect(page.all('[data-testid="dropdown-item"]').size).to be > 1
+ end
end
end
@@ -319,7 +330,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
wait_for_requests
page.within('.mr-section-container') do
- expect(page).to have_content('Merge failed: Something went wrong')
+ expect(page).to have_content('Something went wrong.')
end
end
end
@@ -340,7 +351,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
wait_for_requests
page.within('.mr-section-container') do
- expect(page).to have_content('Merge failed: Something went wrong')
+ expect(page).to have_content('Something went wrong.')
end
end
end
@@ -377,7 +388,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
click_button 'Merge'
page.within('.mr-widget-body') do
- expect(page).to have_content('Conflicts detected during merge')
+ expect(page).to have_content('An error occurred while merging')
end
end
end
diff --git a/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb b/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb
index 04d8c52df61..1ef6d2a1068 100644
--- a/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb
+++ b/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb
@@ -9,152 +9,166 @@ RSpec.describe 'Merge request < User sees mini pipeline graph', :js do
let(:pipeline) { create(:ci_empty_pipeline, project: project, ref: 'master', status: 'running', sha: project.commit.id) }
let(:build) { create(:ci_build, pipeline: pipeline, stage: 'test') }
- before do
- build.run
- build.trace.set('hello')
- sign_in(user)
- visit_merge_request
- end
-
- def visit_merge_request(format: :html, serializer: nil)
- visit project_merge_request_path(project, merge_request, format: format, serializer: serializer)
- end
-
- it 'displays a mini pipeline graph' do
- expect(page).to have_selector('.mr-widget-pipeline-graph')
- end
-
- context 'as json' do
- let(:artifacts_file1) { fixture_file_upload(File.join('spec/fixtures/banana_sample.gif'), 'image/gif') }
- let(:artifacts_file2) { fixture_file_upload(File.join('spec/fixtures/dk.png'), 'image/png') }
-
+ shared_examples 'mini pipeline renders' do |ci_mini_pipeline_gl_dropdown_enabled|
before do
- job = create(:ci_build, :success, :trace_artifact, pipeline: pipeline)
- create(:ci_job_artifact, :archive, file: artifacts_file1, job: job)
- create(:ci_build, :manual, pipeline: pipeline, when: 'manual')
+ build.run
+ build.trace.set('hello')
+ sign_in(user)
+ stub_feature_flags(ci_mini_pipeline_gl_dropdown: ci_mini_pipeline_gl_dropdown_enabled)
+ visit_merge_request
end
- # TODO: https://gitlab.com/gitlab-org/gitlab-foss/issues/48034
- xit 'avoids repeated database queries' do
- before = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') }
-
- job = create(:ci_build, :success, :trace_artifact, pipeline: pipeline)
- create(:ci_job_artifact, :archive, file: artifacts_file2, job: job)
- create(:ci_build, :manual, pipeline: pipeline, when: 'manual')
-
- after = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') }
+ let_it_be(:dropdown_toggle_selector) do
+ if ci_mini_pipeline_gl_dropdown_enabled
+ '[data-testid="mini-pipeline-graph-dropdown"] .dropdown-toggle'
+ else
+ '[data-testid="mini-pipeline-graph-dropdown-toggle"]'
+ end
+ end
- expect(before.count).to eq(after.count)
- expect(before.cached_count).to eq(after.cached_count)
+ def visit_merge_request(format: :html, serializer: nil)
+ visit project_merge_request_path(project, merge_request, format: format, serializer: serializer)
end
- end
- describe 'build list toggle' do
- let(:toggle) do
- find('.mini-pipeline-graph-dropdown-toggle')
- first('.mini-pipeline-graph-dropdown-toggle')
+ it 'displays a mini pipeline graph' do
+ expect(page).to have_selector('.mr-widget-pipeline-graph')
end
- # Status icon button styles should update as described in
- # https://gitlab.com/gitlab-org/gitlab-foss/issues/42769
- it 'has unique styles for default, :hover, :active, and :focus states' do
- find('.mini-pipeline-graph-dropdown-toggle')
- default_background_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('background-color');")
- default_foreground_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible svg').css('fill');")
- default_box_shadow = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('box-shadow');")
+ context 'as json' do
+ let(:artifacts_file1) { fixture_file_upload(File.join('spec/fixtures/banana_sample.gif'), 'image/gif') }
+ let(:artifacts_file2) { fixture_file_upload(File.join('spec/fixtures/dk.png'), 'image/png') }
- toggle.hover
+ before do
+ job = create(:ci_build, :success, :trace_artifact, pipeline: pipeline)
+ create(:ci_job_artifact, :archive, file: artifacts_file1, job: job)
+ create(:ci_build, :manual, pipeline: pipeline, when: 'manual')
+ end
- find('.mini-pipeline-graph-dropdown-toggle')
- hover_background_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('background-color');")
- hover_foreground_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible svg').css('fill');")
- hover_box_shadow = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('box-shadow');")
+ # TODO: https://gitlab.com/gitlab-org/gitlab-foss/issues/48034
+ xit 'avoids repeated database queries' do
+ before = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') }
- page.driver.browser.action.click_and_hold(toggle.native).perform
+ job = create(:ci_build, :success, :trace_artifact, pipeline: pipeline)
+ create(:ci_job_artifact, :archive, file: artifacts_file2, job: job)
+ create(:ci_build, :manual, pipeline: pipeline, when: 'manual')
- find('.mini-pipeline-graph-dropdown-toggle')
- active_background_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('background-color');")
- active_foreground_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible svg').css('fill');")
- active_box_shadow = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('box-shadow');")
+ after = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') }
- page.driver.browser.action.release(toggle.native)
- .move_by(100, 100)
- .perform
+ expect(before.count).to eq(after.count)
+ expect(before.cached_count).to eq(after.cached_count)
+ end
+ end
- find('.mini-pipeline-graph-dropdown-toggle')
- focus_background_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('background-color');")
- focus_foreground_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible svg').css('fill');")
- focus_box_shadow = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('box-shadow');")
+ describe 'build list toggle' do
+ let(:toggle) do
+ find(dropdown_toggle_selector)
+ first(dropdown_toggle_selector)
+ end
- expect(default_background_color).not_to eq(hover_background_color)
- expect(hover_background_color).not_to eq(active_background_color)
- expect(default_background_color).not_to eq(active_background_color)
+ # Status icon button styles should update as described in
+ # https://gitlab.com/gitlab-org/gitlab-foss/issues/42769
+ it 'has unique styles for default, :hover, :active, and :focus states' do
+ default_background_color, default_foreground_color, default_box_shadow = get_toggle_colors(dropdown_toggle_selector)
- expect(default_foreground_color).not_to eq(hover_foreground_color)
- expect(hover_foreground_color).not_to eq(active_foreground_color)
- expect(default_foreground_color).not_to eq(active_foreground_color)
+ toggle.hover
+ hover_background_color, hover_foreground_color, hover_box_shadow = get_toggle_colors(dropdown_toggle_selector)
- expect(focus_background_color).to eq(hover_background_color)
- expect(focus_foreground_color).to eq(hover_foreground_color)
+ page.driver.browser.action.click_and_hold(toggle.native).perform
+ active_background_color, active_foreground_color, active_box_shadow = get_toggle_colors(dropdown_toggle_selector)
+ page.driver.browser.action.release(toggle.native).perform
- expect(default_box_shadow).to eq('none')
- expect(hover_box_shadow).to eq('none')
- expect(active_box_shadow).not_to eq('none')
- expect(focus_box_shadow).not_to eq('none')
- end
+ page.driver.browser.action.click(toggle.native).move_by(100, 100).perform
+ focus_background_color, focus_foreground_color, focus_box_shadow = get_toggle_colors(dropdown_toggle_selector)
- it 'shows tooltip when hovered' do
- toggle.hover
+ expect(default_background_color).not_to eq(hover_background_color)
+ expect(hover_background_color).not_to eq(active_background_color)
+ expect(default_background_color).not_to eq(active_background_color)
- expect(page).to have_selector('.tooltip')
- end
- end
+ expect(default_foreground_color).not_to eq(hover_foreground_color)
+ expect(hover_foreground_color).not_to eq(active_foreground_color)
+ expect(default_foreground_color).not_to eq(active_foreground_color)
- describe 'builds list menu' do
- let(:toggle) do
- find('.mini-pipeline-graph-dropdown-toggle')
- first('.mini-pipeline-graph-dropdown-toggle')
- end
+ expect(focus_background_color).to eq(hover_background_color)
+ expect(focus_foreground_color).to eq(hover_foreground_color)
- before do
- toggle.click
- wait_for_requests
- end
+ expect(default_box_shadow).to eq('none')
+ expect(hover_box_shadow).to eq('none')
+ expect(active_box_shadow).not_to eq('none')
+ expect(focus_box_shadow).not_to eq('none')
+ end
+
+ it 'shows tooltip when hovered' do
+ toggle.hover
- it 'pens when toggle is clicked' do
- expect(toggle.find(:xpath, '..')).to have_selector('.mini-pipeline-graph-dropdown-menu')
+ expect(page).to have_selector('.tooltip')
+ end
end
- it 'closes when toggle is clicked again' do
- toggle.click
+ describe 'builds list menu' do
+ let(:toggle) do
+ find(dropdown_toggle_selector)
+ first(dropdown_toggle_selector)
+ end
- expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu')
- end
+ before do
+ toggle.click
+ wait_for_requests
+ end
- it 'closes when clicking somewhere else' do
- find('body').click
+ it 'pens when toggle is clicked' do
+ expect(toggle.find(:xpath, '..')).to have_selector('.mini-pipeline-graph-dropdown-menu')
+ end
- expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu')
- end
+ it 'closes when toggle is clicked again' do
+ toggle.click
- describe 'build list build item' do
- let(:build_item) do
- find('.mini-pipeline-graph-dropdown-item')
- first('.mini-pipeline-graph-dropdown-item')
+ expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu')
end
- it 'visits the build page when clicked' do
- build_item.click
- find('.build-page')
+ it 'closes when clicking somewhere else' do
+ find('body').click
- expect(current_path).to eql(project_job_path(project, build))
+ expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu')
end
- it 'shows tooltip when hovered' do
- build_item.hover
+ describe 'build list build item' do
+ let(:build_item) do
+ find('.mini-pipeline-graph-dropdown-item')
+ first('.mini-pipeline-graph-dropdown-item')
+ end
- expect(page).to have_selector('.tooltip')
+ it 'visits the build page when clicked' do
+ build_item.click
+ find('.build-page')
+
+ expect(current_path).to eql(project_job_path(project, build))
+ end
+
+ it 'shows tooltip when hovered' do
+ build_item.hover
+
+ expect(page).to have_selector('.tooltip')
+ end
end
end
end
+
+ context 'with ci_mini_pipeline_gl_dropdown disabled' do
+ it_behaves_like "mini pipeline renders", false
+ end
+
+ context 'with ci_mini_pipeline_gl_dropdown enabled' do
+ it_behaves_like "mini pipeline renders", true
+ end
+
+ private
+
+ def get_toggle_colors(selector)
+ find(selector)
+ [
+ evaluate_script("$('#{selector}:visible').css('background-color');"),
+ evaluate_script("$('#{selector}:visible svg').css('fill');"),
+ evaluate_script("$('#{selector}:visible').css('box-shadow');")
+ ]
+ end
end
diff --git a/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb b/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb
index a2ec34335ec..bbeb91bbd19 100644
--- a/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb
+++ b/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb
@@ -73,6 +73,23 @@ RSpec.describe 'User comments on a diff', :js do
end
end
+ it 'allows suggestions in replies' do
+ click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
+
+ page.within('.js-discussion-note-form') do
+ fill_in('note_note', with: "```suggestion\n# change to a comment\n```")
+ click_button('Add comment now')
+ end
+
+ wait_for_requests
+
+ click_button 'Reply...'
+
+ find('.js-suggestion-btn').click
+
+ expect(find('.js-vue-issue-note-form').value).to include("url = https://github.com/gitlabhq/gitlab-shell.git")
+ end
+
it 'suggestion is appliable' do
click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
diff --git a/spec/features/merge_request/user_views_open_merge_request_spec.rb b/spec/features/merge_request/user_views_open_merge_request_spec.rb
index e8998f9457a..9bda48a3ec5 100644
--- a/spec/features/merge_request/user_views_open_merge_request_spec.rb
+++ b/spec/features/merge_request/user_views_open_merge_request_spec.rb
@@ -30,19 +30,6 @@ RSpec.describe 'User views an open merge request' do
end
end
- context 'when merge_request_reviewers is turned off' do
- let(:project) { create(:project, :public, :repository) }
-
- before do
- stub_feature_flags(merge_request_reviewers: false)
- visit(merge_request_path(merge_request))
- end
-
- it 'has reviewers in sidebar' do
- expect(page).not_to have_css('.reviewer')
- end
- end
-
context 'when a merge request has repository', :js do
let(:project) { create(:project, :public, :repository) }
@@ -107,5 +94,21 @@ RSpec.describe 'User views an open merge request' do
end
end
end
+
+ context 'when the assignee\'s availability set' do
+ before do
+ merge_request.author.create_status(availability: 'busy')
+ merge_request.assignees << merge_request.author
+
+ visit(merge_request_path(merge_request))
+ end
+
+ it 'exposes the availability in the data-availability attribute' do
+ assignees_data = find_all("input[name='merge_request[assignee_ids][]']", visible: false)
+
+ expect(assignees_data.size).to eq(1)
+ expect(assignees_data.first['data-availability']).to eq('busy')
+ end
+ end
end
end
diff --git a/spec/features/merge_requests/user_filters_by_milestones_spec.rb b/spec/features/merge_requests/user_filters_by_milestones_spec.rb
index 41a0b0012d1..877d5e6a4ee 100644
--- a/spec/features/merge_requests/user_filters_by_milestones_spec.rb
+++ b/spec/features/merge_requests/user_filters_by_milestones_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe 'Merge Requests > User filters by milestones', :js do
let(:milestone) { create(:milestone, project: project) }
before do
- create(:merge_request, :with_diffs, source_project: project)
+ create(:merge_request, source_project: project)
create(:merge_request, :simple, source_project: project, milestone: milestone)
sign_in(user)
diff --git a/spec/features/merge_requests/user_lists_merge_requests_spec.rb b/spec/features/merge_requests/user_lists_merge_requests_spec.rb
index 36d28ae2822..6b8dcd7dbb6 100644
--- a/spec/features/merge_requests/user_lists_merge_requests_spec.rb
+++ b/spec/features/merge_requests/user_lists_merge_requests_spec.rb
@@ -40,9 +40,8 @@ RSpec.describe 'Merge requests > User lists merge requests' do
updated_at: 10.seconds.ago)
end
- context 'when merge_request_reviewers is turned on' do
+ context 'merge request reviewers' do
before do
- stub_feature_flags(merge_request_reviewers: true)
visit_merge_requests(project, reviewer_id: user.id)
end
@@ -62,15 +61,6 @@ RSpec.describe 'Merge requests > User lists merge requests' do
end
end
- context 'when merge_request_reviewers is turned false' do
- it 'has no reviewers in MR list' do
- stub_feature_flags(merge_request_reviewers: false)
- visit_merge_requests(project, reviewer_id: user.id)
-
- expect(page).not_to have_css('.issuable-reviewers')
- end
- end
-
it 'filters on no assignee' do
visit_merge_requests(project, assignee_id: IssuableFinder::Params::FILTER_NONE)
diff --git a/spec/features/profiles/user_edit_preferences_spec.rb b/spec/features/profiles/user_edit_preferences_spec.rb
index d489d92c524..3129e4bd952 100644
--- a/spec/features/profiles/user_edit_preferences_spec.rb
+++ b/spec/features/profiles/user_edit_preferences_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'User edit preferences profile' do
+RSpec.describe 'User edit preferences profile', :js do
let(:user) { create(:user) }
before do
@@ -53,7 +53,14 @@ RSpec.describe 'User edit preferences profile' do
fill_in 'Tab width', with: -1
click_button 'Save changes'
- expect(page).to have_content('Failed to save preferences')
+ field = page.find_field('user[tab_width]')
+ message = field.native.attribute("validationMessage")
+ expect(message).to eq "Value must be greater than or equal to 1."
+
+ # User trying to hack an invalid value
+ page.execute_script("document.querySelector('#user_tab_width').setAttribute('min', '-1')")
+ click_button 'Save changes'
+ expect(page).to have_content('Failed to save preferences.')
end
end
diff --git a/spec/features/profiles/user_edit_profile_spec.rb b/spec/features/profiles/user_edit_profile_spec.rb
index 239bc04a9cb..bd4917824d1 100644
--- a/spec/features/profiles/user_edit_profile_spec.rb
+++ b/spec/features/profiles/user_edit_profile_spec.rb
@@ -199,6 +199,38 @@ RSpec.describe 'User edit profile' do
expect(busy_status.checked?).to eq(true)
end
+ context 'with user status set to busy' do
+ let(:project) { create(:project, :public) }
+ let(:issue) { create(:issue, project: project, author: user) }
+
+ before do
+ toggle_busy_status
+ submit_settings
+
+ project.add_developer(user)
+ visit project_issue_path(project, issue)
+ end
+
+ it 'shows author as busy in the assignee dropdown' do
+ find('.block.assignee .edit-link').click
+ wait_for_requests
+
+ page.within '.dropdown-menu-user' do
+ expect(page).to have_content("#{user.name} (Busy)")
+ end
+ end
+
+ it 'displays the assignee busy status' do
+ click_button 'assign yourself'
+ wait_for_requests
+
+ visit project_issue_path(project, issue)
+ wait_for_requests
+
+ expect(page.find('[data-testid="expanded-assignee"]')).to have_text("#{user.name} (Busy)")
+ end
+ end
+
context 'with set_user_availability_status feature flag disabled' do
before do
stub_feature_flags(set_user_availability_status: false)
diff --git a/spec/features/profiles/user_search_settings_spec.rb b/spec/features/profiles/user_search_settings_spec.rb
new file mode 100644
index 00000000000..60df0d7532b
--- /dev/null
+++ b/spec/features/profiles/user_search_settings_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'User searches their settings', :js do
+ let_it_be(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ context 'in profile page' do
+ let(:visit_path) { profile_path }
+
+ it_behaves_like 'can search settings with feature flag check', 'Public Avatar', 'Main settings'
+ end
+
+ context 'in preferences page' do
+ before do
+ visit profile_preferences_path
+ end
+
+ it_behaves_like 'can search settings', 'Syntax highlighting theme', 'Behavior'
+ end
+end
diff --git a/spec/features/profiles/user_visits_notifications_tab_spec.rb b/spec/features/profiles/user_visits_notifications_tab_spec.rb
index 997cc8e3c4b..289fbff0404 100644
--- a/spec/features/profiles/user_visits_notifications_tab_spec.rb
+++ b/spec/features/profiles/user_visits_notifications_tab_spec.rb
@@ -7,6 +7,7 @@ RSpec.describe 'User visits the notifications tab', :js do
let(:user) { create(:user) }
before do
+ stub_feature_flags(vue_notification_dropdown: false)
project.add_maintainer(user)
sign_in(user)
visit(profile_notifications_path)
diff --git a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
index 56db7efff51..da63f7c0f41 100644
--- a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
+++ b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User visits the profile preferences page' do
+RSpec.describe 'User visits the profile preferences page', :js do
include Select2Helper
let(:user) { create(:user) }
@@ -39,7 +39,7 @@ RSpec.describe 'User visits the profile preferences page' do
describe 'User changes their default dashboard', :js do
it 'creates a flash message' do
select2('stars', from: '#user_dashboard')
- click_button 'Save'
+ click_button 'Save changes'
wait_for_requests
@@ -48,7 +48,7 @@ RSpec.describe 'User visits the profile preferences page' do
it 'updates their preference' do
select2('stars', from: '#user_dashboard')
- click_button 'Save'
+ click_button 'Save changes'
wait_for_requests
@@ -67,7 +67,7 @@ RSpec.describe 'User visits the profile preferences page' do
describe 'User changes their language', :js do
it 'creates a flash message', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/31404' do
select2('en', from: '#user_preferred_language')
- click_button 'Save'
+ click_button 'Save changes'
wait_for_requests
@@ -77,7 +77,7 @@ RSpec.describe 'User visits the profile preferences page' do
it 'updates their preference' do
wait_for_requests
select2('pt_BR', from: '#user_preferred_language')
- click_button 'Save'
+ click_button 'Save changes'
wait_for_requests
refresh
@@ -94,6 +94,8 @@ RSpec.describe 'User visits the profile preferences page' do
click_button 'Save changes'
+ wait_for_requests
+
expect(user.reload.render_whitespace_in_code).to be(true)
expect(render_whitespace_field).to be_checked
end
diff --git a/spec/features/profiles/user_visits_profile_spec.rb b/spec/features/profiles/user_visits_profile_spec.rb
index 77da1f138c7..475fda5e7a1 100644
--- a/spec/features/profiles/user_visits_profile_spec.rb
+++ b/spec/features/profiles/user_visits_profile_spec.rb
@@ -39,7 +39,7 @@ RSpec.describe 'User visits their profile' do
find(:css, '.header-user-dropdown-toggle').click
page.within ".header-user" do
- click_link "Profile"
+ click_link user.username
end
end
diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb
index dcad7ee66a3..4bfe8852291 100644
--- a/spec/features/projects/branches_spec.rb
+++ b/spec/features/projects/branches_spec.rb
@@ -21,11 +21,11 @@ RSpec.describe 'Branches' do
before do
# Add 4 stale branches
(1..4).reverse_each do |i|
- travel_to((threshold + i).ago) { create_file(message: "a commit in stale-#{i}", branch_name: "stale-#{i}") }
+ travel_to((threshold + i.hours).ago) { create_file(message: "a commit in stale-#{i}", branch_name: "stale-#{i}") }
end
# Add 6 active branches
(1..6).each do |i|
- travel_to((threshold - i).ago) { create_file(message: "a commit in active-#{i}", branch_name: "active-#{i}") }
+ travel_to((threshold - i.hours).ago) { create_file(message: "a commit in active-#{i}", branch_name: "active-#{i}") }
end
end
@@ -34,7 +34,7 @@ RSpec.describe 'Branches' do
visit project_branches_path(project)
expect(page).to have_content(sorted_branches(repository, count: 5, sort_by: :updated_desc, state: 'active'))
- expect(page).to have_content(sorted_branches(repository, count: 4, sort_by: :updated_desc, state: 'stale'))
+ expect(page).to have_content(sorted_branches(repository, count: 4, sort_by: :updated_asc, state: 'stale'))
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')
@@ -50,10 +50,10 @@ RSpec.describe 'Branches' do
end
describe 'Stale branches page' do
- it 'shows 4 active branches sorted by last updated' do
+ it 'shows 4 stale branches sorted by last updated' do
visit project_branches_filtered_path(project, state: 'stale')
- expect(page).to have_content(sorted_branches(repository, count: 4, sort_by: :updated_desc, state: 'stale'))
+ expect(page).to have_content(sorted_branches(repository, count: 4, sort_by: :updated_asc, state: 'stale'))
end
end
diff --git a/spec/features/projects/commit/cherry_pick_spec.rb b/spec/features/projects/commit/cherry_pick_spec.rb
index 9fe3f4cd63e..489a90cc8fc 100644
--- a/spec/features/projects/commit/cherry_pick_spec.rb
+++ b/spec/features/projects/commit/cherry_pick_spec.rb
@@ -2,108 +2,126 @@
require 'spec_helper'
-RSpec.describe 'Cherry-pick Commits' do
- let(:user) { create(:user) }
- let(:group) { create(:group) }
- let(:project) { create(:project, :repository, namespace: group) }
- let(:master_pickable_commit) { project.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') }
- let(:master_pickable_merge) { project.commit('e56497bb5f03a90a51293fc6d516788730953899') }
+RSpec.describe 'Cherry-pick Commits', :js do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:sha) { '7d3b0f7cff5f37573aea97cebfd5692ea1689924' }
+ let!(:project) { create_default(:project, :repository, namespace: user.namespace) }
+ let(:master_pickable_commit) { project.commit(sha) }
before do
sign_in(user)
- project.add_maintainer(user)
- visit project_commit_path(project, master_pickable_commit.id)
end
- context "I cherry-pick a commit" do
- it do
- find("a[href='#modal-cherry-pick-commit']").click
- expect(page).not_to have_content('v1.0.0') # Only branches, not tags
- page.within('#modal-cherry-pick-commit') do
- uncheck 'create_merge_request'
- click_button 'Cherry-pick'
- end
- expect(page).to have_content('The commit has been successfully cherry-picked into master.')
- end
- end
+ context 'when clicking cherry-pick from the dropdown for a commit on pipelines tab' do
+ it 'launches the modal form' do
+ create(:ci_empty_pipeline, sha: sha)
+ visit project_commit_path(project, master_pickable_commit.id)
+ click_link 'Pipelines'
- context "I cherry-pick a merge commit" do
- it do
- find("a[href='#modal-cherry-pick-commit']").click
- page.within('#modal-cherry-pick-commit') do
- uncheck 'create_merge_request'
- click_button 'Cherry-pick'
+ open_modal
+
+ page.within(modal_selector) do
+ expect(page).to have_content('Cherry-pick this commit')
end
- expect(page).to have_content('The commit has been successfully cherry-picked into master.')
end
end
- context "I cherry-pick a commit that was previously cherry-picked" do
- it do
- find("a[href='#modal-cherry-pick-commit']").click
- page.within('#modal-cherry-pick-commit') do
- uncheck 'create_merge_request'
- click_button 'Cherry-pick'
- end
+ context 'when starting from the commit tab' do
+ before do
visit project_commit_path(project, master_pickable_commit.id)
- find("a[href='#modal-cherry-pick-commit']").click
- page.within('#modal-cherry-pick-commit') do
- uncheck 'create_merge_request'
- click_button 'Cherry-pick'
- end
- expect(page).to have_content('Sorry, we cannot cherry-pick this commit automatically.')
end
- end
- context "I cherry-pick a commit in a new merge request", :js do
- it do
- find('.header-action-buttons a.dropdown-toggle').click
- find("a[href='#modal-cherry-pick-commit']").click
- page.within('#modal-cherry-pick-commit') do
- click_button 'Cherry-pick'
+ context 'when cherry-picking a commit' do
+ specify do
+ cherry_pick_commit
+
+ expect(page).to have_content('The commit has been successfully cherry-picked into master.')
end
+ end
- wait_for_requests
+ context 'when cherry-picking a merge commit' do
+ specify do
+ cherry_pick_commit
- expect(page).to have_content("The commit has been successfully cherry-picked into cherry-pick-#{master_pickable_commit.short_id}. You can now submit a merge request to get this change into the original branch.")
- expect(page).to have_content("From cherry-pick-#{master_pickable_commit.short_id} into master")
+ expect(page).to have_content('The commit has been successfully cherry-picked into master.')
+ end
end
- end
- context "I cherry-pick a commit from a different branch", :js do
- it do
- find('.header-action-buttons a.dropdown-toggle').click
- find(:css, "a[href='#modal-cherry-pick-commit']").click
+ context 'when cherry-picking a commit that was previously cherry-picked' do
+ specify do
+ cherry_pick_commit
- page.within('#modal-cherry-pick-commit') do
- click_button 'master'
+ visit project_commit_path(project, master_pickable_commit.id)
+
+ cherry_pick_commit
+
+ expect(page).to have_content('Sorry, we cannot cherry-pick this commit automatically.')
end
+ end
- wait_for_requests
+ context 'when cherry-picking a commit in a new merge request' do
+ specify do
+ cherry_pick_commit(create_merge_request: true)
- page.within('#modal-cherry-pick-commit .dropdown-menu') do
- find('.dropdown-input input').set('feature')
- wait_for_requests
- click_link "feature"
+ expect(page).to have_content("The commit has been successfully cherry-picked into cherry-pick-#{master_pickable_commit.short_id}. You can now submit a merge request to get this change into the original branch.")
+ expect(page).to have_content("From cherry-pick-#{master_pickable_commit.short_id} into master")
end
+ end
- page.within('#modal-cherry-pick-commit') do
- uncheck 'create_merge_request'
- click_button 'Cherry-pick'
+ context 'when I cherry-picking a commit from a different branch' do
+ specify do
+ open_modal
+
+ page.within(modal_selector) do
+ click_button 'master'
+ end
+
+ page.within("#{modal_selector} .dropdown-menu") do
+ find('[data-testid="dropdown-search-box"]').set('feature')
+ wait_for_requests
+ click_button 'feature'
+ end
+
+ submit_cherry_pick
+
+ expect(page).to have_content('The commit has been successfully cherry-picked into feature.')
end
+ end
+
+ context 'when the project is archived' do
+ let(:project) { create(:project, :repository, :archived, namespace: user.namespace) }
- expect(page).to have_content('The commit has been successfully cherry-picked into feature.')
+ it 'does not show the cherry-pick link' do
+ open_dropdown
+
+ expect(page).not_to have_text("Cherry-pick")
+ end
end
end
- context 'when the project is archived' do
- let(:project) { create(:project, :repository, :archived, namespace: group) }
+ def cherry_pick_commit(create_merge_request: false)
+ open_modal
- it 'does not show the cherry-pick link' do
- find('.header-action-buttons a.dropdown-toggle').click
+ submit_cherry_pick(create_merge_request: create_merge_request)
+ end
+
+ def open_dropdown
+ find('.header-action-buttons .dropdown').click
+ end
- expect(page).not_to have_text("Cherry-pick")
- expect(page).not_to have_css("a[href='#modal-cherry-pick-commit']")
+ def open_modal
+ open_dropdown
+ find('[data-testid="cherry-pick-commit-link"]').click
+ end
+
+ def submit_cherry_pick(create_merge_request: false)
+ page.within(modal_selector) do
+ uncheck('create_merge_request') unless create_merge_request
+ click_button('Cherry-pick')
end
end
+
+ def modal_selector
+ '[data-testid="modal-commit"]'
+ end
end
diff --git a/spec/features/projects/commit/user_reverts_commit_spec.rb b/spec/features/projects/commit/user_reverts_commit_spec.rb
index f3c364dab97..72c639a027e 100644
--- a/spec/features/projects/commit/user_reverts_commit_spec.rb
+++ b/spec/features/projects/commit/user_reverts_commit_spec.rb
@@ -6,58 +6,89 @@ RSpec.describe 'User reverts a commit', :js do
include RepoHelpers
let_it_be(:user) { create(:user) }
- let(:project) { create(:project, :repository, namespace: user.namespace) }
+ let!(:project) { create_default(:project, :repository, namespace: user.namespace) }
before do
sign_in(user)
-
- visit(project_commit_path(project, sample_commit.id))
end
- def revert_commit(create_merge_request: false)
- find('.header-action-buttons .dropdown').click
- find('[data-testid="revert-commit-link"]').click
+ context 'when clicking revert from the dropdown for a commit on pipelines tab' do
+ it 'launches the modal and is able to submit the revert' do
+ sha = '7d3b0f7cff5f37573aea97cebfd5692ea1689924'
+ create(:ci_empty_pipeline, sha: sha)
+ visit project_commit_path(project, project.commit(sha).id)
+ click_link 'Pipelines'
- page.within('[data-testid="modal-commit"]') do
- uncheck('create_merge_request') unless create_merge_request
- click_button('Revert')
+ open_modal
+
+ page.within(modal_selector) do
+ expect(page).to have_content('Revert this commit')
+ end
end
end
- context 'without creating a new merge request' do
- it 'reverts a commit' do
- revert_commit
+ context 'when starting from the commit tab' do
+ before do
+ visit project_commit_path(project, sample_commit.id)
+ end
+
+ context 'without creating a new merge request' do
+ it 'reverts a commit' do
+ revert_commit
+
+ expect(page).to have_content('The commit has been successfully reverted.')
+ end
+
+ it 'does not revert a previously reverted commit' do
+ revert_commit
+ # Visit the comment again once it was reverted.
+ visit project_commit_path(project, sample_commit.id)
+
+ revert_commit
- expect(page).to have_content('The commit has been successfully reverted.')
+ expect(page).to have_content('Sorry, we cannot revert this commit automatically.')
+ end
end
- it 'does not revert a previously reverted commit' do
- revert_commit
- # Visit the comment again once it was reverted.
- visit project_commit_path(project, sample_commit.id)
+ context 'with creating a new merge request' do
+ it 'reverts a commit' do
+ revert_commit(create_merge_request: true)
+
+ expect(page).to have_content('The commit has been successfully reverted. You can now submit a merge request to get this change into the original branch.')
+ expect(page).to have_content("From revert-#{Commit.truncate_sha(sample_commit.id)} into master")
+ end
+ end
- revert_commit
+ context 'when the project is archived' do
+ let(:project) { create(:project, :repository, :archived, namespace: user.namespace) }
- expect(page).to have_content('Sorry, we cannot revert this commit automatically.')
+ it 'does not show the revert link' do
+ open_dropdown
+
+ expect(page).not_to have_link('Revert')
+ end
end
end
- context 'with creating a new merge request' do
- it 'reverts a commit' do
- revert_commit(create_merge_request: true)
+ def revert_commit(create_merge_request: false)
+ open_modal
- expect(page).to have_content('The commit has been successfully reverted. You can now submit a merge request to get this change into the original branch.')
- expect(page).to have_content("From revert-#{Commit.truncate_sha(sample_commit.id)} into master")
+ page.within(modal_selector) do
+ uncheck('create_merge_request') unless create_merge_request
+ click_button('Revert')
end
end
- context 'when the project is archived' do
- let(:project) { create(:project, :repository, :archived, namespace: user.namespace) }
+ def open_dropdown
+ find('.header-action-buttons .dropdown').click
+ end
- it 'does not show the revert link' do
- find('.header-action-buttons .dropdown').click
+ def open_modal
+ open_dropdown
+ find('[data-testid="revert-commit-link"]').click
+ end
- expect(page).not_to have_link('Revert')
- end
+ def modal_selector
+ '[data-testid="modal-commit"]'
end
end
diff --git a/spec/features/projects/commits/user_browses_commits_spec.rb b/spec/features/projects/commits/user_browses_commits_spec.rb
index 596b4773716..4894e2b7f3e 100644
--- a/spec/features/projects/commits/user_browses_commits_spec.rb
+++ b/spec/features/projects/commits/user_browses_commits_spec.rb
@@ -203,10 +203,11 @@ RSpec.describe 'User browses commits' do
context 'when click the compare tab' do
before do
+ wait_for_requests
click_link('Compare')
end
- it 'does not render create merge request button' do
+ it 'does not render create merge request button', :js do
expect(page).not_to have_link 'Create merge request'
end
end
@@ -236,10 +237,11 @@ RSpec.describe 'User browses commits' do
context 'when click the compare tab' do
before do
+ wait_for_requests
click_link('Compare')
end
- it 'renders create merge request button' do
+ it 'renders create merge request button', :js do
expect(page).to have_link 'Create merge request'
end
end
@@ -276,10 +278,11 @@ RSpec.describe 'User browses commits' do
context 'when click the compare tab' do
before do
+ wait_for_requests
click_link('Compare')
end
- it 'renders button to the merge request' do
+ it 'renders button to the merge request', :js do
expect(page).not_to have_link 'Create merge request'
expect(page).to have_link 'View open merge request', href: project_merge_request_path(project, merge_request)
end
diff --git a/spec/features/projects/compare_spec.rb b/spec/features/projects/compare_spec.rb
index e387ea4d473..64e9968061c 100644
--- a/spec/features/projects/compare_spec.rb
+++ b/spec/features/projects/compare_spec.rb
@@ -17,10 +17,10 @@ RSpec.describe "Compare", :js do
visit project_compare_index_path(project, from: 'master', to: 'master')
select_using_dropdown 'from', 'feature'
- expect(find('.js-compare-from-dropdown .dropdown-toggle-text')).to have_content('feature')
+ expect(find('.js-compare-from-dropdown .gl-new-dropdown-button-text')).to have_content('feature')
select_using_dropdown 'to', 'binary-encoding'
- expect(find('.js-compare-to-dropdown .dropdown-toggle-text')).to have_content('binary-encoding')
+ expect(find('.js-compare-to-dropdown .gl-new-dropdown-button-text')).to have_content('binary-encoding')
click_button 'Compare'
@@ -32,8 +32,8 @@ RSpec.describe "Compare", :js do
it "pre-populates fields" do
visit project_compare_index_path(project, from: "master", to: "master")
- expect(find(".js-compare-from-dropdown .dropdown-toggle-text")).to have_content("master")
- expect(find(".js-compare-to-dropdown .dropdown-toggle-text")).to have_content("master")
+ expect(find(".js-compare-from-dropdown .gl-new-dropdown-button-text")).to have_content("master")
+ expect(find(".js-compare-to-dropdown .gl-new-dropdown-button-text")).to have_content("master")
end
it_behaves_like 'compares branches'
@@ -99,7 +99,7 @@ RSpec.describe "Compare", :js do
find(".js-compare-from-dropdown .compare-dropdown-toggle").click
- expect(find(".js-compare-from-dropdown .dropdown-content")).to have_selector("li", count: 3)
+ expect(find(".js-compare-from-dropdown .gl-new-dropdown-contents")).to have_selector('li.gl-new-dropdown-item', count: 1)
end
context 'when commit has overflow', :js do
@@ -125,10 +125,10 @@ RSpec.describe "Compare", :js do
visit project_compare_index_path(project, from: "master", to: "master")
select_using_dropdown "from", "v1.0.0"
- expect(find(".js-compare-from-dropdown .dropdown-toggle-text")).to have_content("v1.0.0")
+ expect(find(".js-compare-from-dropdown .gl-new-dropdown-button-text")).to have_content("v1.0.0")
select_using_dropdown "to", "v1.1.0"
- expect(find(".js-compare-to-dropdown .dropdown-toggle-text")).to have_content("v1.1.0")
+ expect(find(".js-compare-to-dropdown .gl-new-dropdown-button-text")).to have_content("v1.1.0")
click_button "Compare"
expect(page).to have_content "Commits"
@@ -136,19 +136,22 @@ RSpec.describe "Compare", :js do
end
def select_using_dropdown(dropdown_type, selection, commit: false)
+ wait_for_requests
+
dropdown = find(".js-compare-#{dropdown_type}-dropdown")
dropdown.find(".compare-dropdown-toggle").click
# find input before using to wait for the inputs visibility
dropdown.find('.dropdown-menu')
dropdown.fill_in("Filter by Git revision", with: selection)
+
wait_for_requests
if commit
- dropdown.find('input[type="search"]').send_keys(:return)
+ dropdown.find('.gl-search-box-by-type-input').send_keys(:return)
else
# find before all to wait for the items visibility
- dropdown.find("a[data-ref=\"#{selection}\"]", match: :first)
- dropdown.all("a[data-ref=\"#{selection}\"]").last.click
+ dropdown.find(".js-compare-#{dropdown_type}-dropdown .dropdown-item", text: selection, match: :first)
+ dropdown.all(".js-compare-#{dropdown_type}-dropdown .dropdown-item", text: selection).first.click
end
end
end
diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb
index 2f0fbd29cb5..c94247f65d2 100644
--- a/spec/features/projects/features_visibility_spec.rb
+++ b/spec/features/projects/features_visibility_spec.rb
@@ -187,7 +187,7 @@ RSpec.describe 'Edit Project Settings' do
click_button "Save changes"
end
- expect(find(".sharing-permissions")).to have_selector(".project-feature-toggle.is-disabled", count: 4)
+ expect(find(".sharing-permissions")).to have_selector(".gl-toggle.is-disabled", minimum: 4)
end
it "shows empty features project homepage" do
@@ -282,10 +282,10 @@ RSpec.describe 'Edit Project Settings' do
end
def toggle_feature_off(feature_name)
- find(".project-feature-controls[data-for=\"#{feature_name}\"] .project-feature-toggle.is-checked").click
+ find(".project-feature-controls[data-for=\"#{feature_name}\"] .gl-toggle.is-checked").click
end
def toggle_feature_on(feature_name)
- find(".project-feature-controls[data-for=\"#{feature_name}\"] .project-feature-toggle:not(.is-checked)").click
+ find(".project-feature-controls[data-for=\"#{feature_name}\"] .gl-toggle:not(.is-checked)").click
end
end
diff --git a/spec/features/projects/files/dockerfile_dropdown_spec.rb b/spec/features/projects/files/dockerfile_dropdown_spec.rb
index 17258f7042f..40d19a94b42 100644
--- a/spec/features/projects/files/dockerfile_dropdown_spec.rb
+++ b/spec/features/projects/files/dockerfile_dropdown_spec.rb
@@ -2,14 +2,16 @@
require 'spec_helper'
-RSpec.describe 'Projects > Files > User wants to add a Dockerfile file', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/297400' do
+RSpec.describe 'Projects > Files > User wants to add a Dockerfile file', :js do
+ include Spec::Support::Helpers::Features::EditorLiteSpecHelpers
+
before do
project = create(:project, :repository)
sign_in project.owner
visit project_new_blob_path(project, 'master', file_name: 'Dockerfile')
end
- it 'user can pick a Dockerfile file from the dropdown', :js do
+ it 'user can pick a Dockerfile file from the dropdown' do
expect(page).to have_css('.dockerfile-selector')
find('.js-dockerfile-selector').click
@@ -24,6 +26,6 @@ RSpec.describe 'Projects > Files > User wants to add a Dockerfile file', quarant
wait_for_requests
expect(page).to have_css('.dockerfile-selector .dropdown-toggle-text', text: 'Apply a template')
- expect(page).to have_content('COPY ./ /usr/local/apache2/htdocs/')
+ expect(editor_get_value).to have_content('COPY ./ /usr/local/apache2/htdocs/')
end
end
diff --git a/spec/features/projects/files/gitignore_dropdown_spec.rb b/spec/features/projects/files/gitignore_dropdown_spec.rb
index 5a39f2bcd98..a9f2463ecf6 100644
--- a/spec/features/projects/files/gitignore_dropdown_spec.rb
+++ b/spec/features/projects/files/gitignore_dropdown_spec.rb
@@ -2,14 +2,16 @@
require 'spec_helper'
-RSpec.describe 'Projects > Files > User wants to add a .gitignore file' do
+RSpec.describe 'Projects > Files > User wants to add a .gitignore file', :js do
+ include Spec::Support::Helpers::Features::EditorLiteSpecHelpers
+
before do
project = create(:project, :repository)
sign_in project.owner
visit project_new_blob_path(project, 'master', file_name: '.gitignore')
end
- it 'user can pick a .gitignore file from the dropdown', :js do
+ it 'user can pick a .gitignore file from the dropdown' do
expect(page).to have_css('.gitignore-selector')
find('.js-gitignore-selector').click
@@ -24,7 +26,7 @@ RSpec.describe 'Projects > Files > User wants to add a .gitignore file' do
wait_for_requests
expect(page).to have_css('.gitignore-selector .dropdown-toggle-text', text: 'Apply a template')
- expect(page).to have_content('/.bundle')
- expect(page).to have_content('config/initializers/secret_token.rb')
+ expect(editor_get_value).to have_content('/.bundle')
+ expect(editor_get_value).to have_content('config/initializers/secret_token.rb')
end
end
diff --git a/spec/features/projects/files/gitlab_ci_syntax_yml_dropdown_spec.rb b/spec/features/projects/files/gitlab_ci_syntax_yml_dropdown_spec.rb
index 6308acb41f5..ca6f03472dd 100644
--- a/spec/features/projects/files/gitlab_ci_syntax_yml_dropdown_spec.rb
+++ b/spec/features/projects/files/gitlab_ci_syntax_yml_dropdown_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Projects > Files > User wants to add a .gitlab-ci.yml file' do
+ include Spec::Support::Helpers::Features::EditorLiteSpecHelpers
+
before do
project = create(:project, :repository)
sign_in project.owner
@@ -34,8 +36,7 @@ RSpec.describe 'Projects > Files > User wants to add a .gitlab-ci.yml file' do
let(:experiment_active) { true }
let(:in_experiment_group) { true }
- it 'allows the user to pick a "Learn CI/CD syntax" template from the dropdown', :js,
- { quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/297347' } } do
+ it 'allows the user to pick a "Learn CI/CD syntax" template from the dropdown', :js do
expect(page).to have_css('.gitlab-ci-syntax-yml-selector')
find('.js-gitlab-ci-syntax-yml-selector').click
@@ -50,7 +51,7 @@ RSpec.describe 'Projects > Files > User wants to add a .gitlab-ci.yml file' do
wait_for_requests
expect(page).to have_css('.gitlab-ci-syntax-yml-selector .dropdown-toggle-text', text: 'Learn CI/CD syntax')
- expect(page).to have_content('You can use artifacts to pass data to jobs in later stages.')
+ expect(editor_get_value).to have_content('You can use artifacts to pass data to jobs in later stages.')
end
end
end
diff --git a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
index 879cb6a65c8..55b9f38d8e7 100644
--- a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
+++ b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
@@ -2,14 +2,16 @@
require 'spec_helper'
-RSpec.describe 'Projects > Files > User wants to add a .gitlab-ci.yml file' do
+RSpec.describe 'Projects > Files > User wants to add a .gitlab-ci.yml file', :js do
+ include Spec::Support::Helpers::Features::EditorLiteSpecHelpers
+
before do
project = create(:project, :repository)
sign_in project.owner
visit project_new_blob_path(project, 'master', file_name: '.gitlab-ci.yml')
end
- it 'user can pick a template from the dropdown', :js do
+ it 'user can pick a template from the dropdown' do
expect(page).to have_css('.gitlab-ci-yml-selector')
find('.js-gitlab-ci-yml-selector').click
@@ -24,7 +26,7 @@ RSpec.describe 'Projects > Files > User wants to add a .gitlab-ci.yml file' do
wait_for_requests
expect(page).to have_css('.gitlab-ci-yml-selector .dropdown-toggle-text', text: 'Apply a template')
- expect(page).to have_content('This file is a template, and might need editing before it works on your project')
- expect(page).to have_content('jekyll build -d test')
+ expect(editor_get_value).to have_content('This file is a template, and might need editing before it works on your project')
+ expect(editor_get_value).to have_content('jekyll build -d test')
end
end
diff --git a/spec/features/projects/fork_spec.rb b/spec/features/projects/fork_spec.rb
index 1e84d1552a1..8d0500f5e13 100644
--- a/spec/features/projects/fork_spec.rb
+++ b/spec/features/projects/fork_spec.rb
@@ -9,22 +9,45 @@ RSpec.describe 'Project fork' do
let(:project) { create(:project, :public, :repository) }
before do
- sign_in user
+ sign_in(user)
end
- it 'allows user to fork project' do
+ it 'allows user to fork project from the project page' do
visit project_path(project)
- expect(page).not_to have_css('a.disabled', text: 'Select')
+ expect(page).not_to have_css('a.disabled', text: 'Fork')
end
- it 'disables fork button when user has exceeded project limit' do
- user.projects_limit = 0
- user.save!
+ context 'user has exceeded personal project limit' do
+ before do
+ user.update!(projects_limit: 0)
+ end
- visit project_path(project)
+ it 'disables fork button on project page' do
+ visit project_path(project)
+
+ expect(page).to have_css('a.disabled', text: 'Fork')
+ end
+
+ context 'with a group to fork to' do
+ let!(:group) { create(:group).tap { |group| group.add_owner(user) } }
+
+ it 'enables fork button on project page' do
+ visit project_path(project)
+
+ expect(page).not_to have_css('a.disabled', text: 'Fork')
+ end
+
+ it 'allows user to fork only to the group on fork page', :js do
+ visit new_project_fork_path(project)
+
+ to_personal_namespace = find('[data-qa-selector=fork_namespace_button].disabled')
+ to_group = find(".fork-groups button[data-qa-name=#{group.name}]")
- expect(page).to have_css('a.disabled', text: 'Fork')
+ expect(to_personal_namespace).not_to be_nil
+ expect(to_group).not_to be_disabled
+ end
+ end
end
context 'forking enabled / disabled in project settings' do
diff --git a/spec/features/projects/graph_spec.rb b/spec/features/projects/graph_spec.rb
index 7b9f79c9f7f..72df84bf905 100644
--- a/spec/features/projects/graph_spec.rb
+++ b/spec/features/projects/graph_spec.rb
@@ -72,9 +72,9 @@ RSpec.describe 'Project Graph', :js do
it 'renders CI graphs' do
expect(page).to have_content 'Overall'
- expect(page).to have_content 'Pipelines for last week'
- expect(page).to have_content 'Pipelines for last month'
- expect(page).to have_content 'Pipelines for last year'
+ expect(page).to have_content 'Last week'
+ expect(page).to have_content 'Last month'
+ expect(page).to have_content 'Last year'
expect(page).to have_content 'Duration for the last 30 commits'
end
end
diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb
index 8f1c31f229f..12c5820a69d 100644
--- a/spec/features/projects/issuable_templates_spec.rb
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -95,7 +95,7 @@ RSpec.describe 'issuable templates', :js do
let(:bug_template_content) { 'this is merge request bug template' }
let(:template_override_warning) { 'Applying a template will replace the existing issue description.' }
let(:updated_description) { 'updated merge request description' }
- let(:merge_request) { create(:merge_request, :with_diffs, source_project: project) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
before do
project.repository.create_file(
@@ -154,7 +154,7 @@ RSpec.describe 'issuable templates', :js do
let(:template_content) { 'this is a test "feature-proposal" template' }
let(:fork_user) { create(:user) }
let(:forked_project) { fork_project(project, fork_user, repository: true) }
- let(:merge_request) { create(:merge_request, :with_diffs, source_project: forked_project, target_project: project) }
+ let(:merge_request) { create(:merge_request, source_project: forked_project, target_project: project) }
before do
sign_out(:user)
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index 1557a8a2d72..7811394b541 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -27,40 +27,12 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state do
describe "GET /:project/jobs" do
context 'with no jobs' do
before do
- stub_experiment(jobs_empty_state: experiment_active)
- stub_experiment_for_subject(jobs_empty_state: in_experiment_group)
-
visit project_jobs_path(project)
end
- context 'when experiment not active' do
- let(:experiment_active) { false }
- let(:in_experiment_group) { false }
-
- it 'shows the empty state control page' do
- expect(page).to have_content('No jobs to show')
- expect(page).to have_link('Get started with Pipelines')
- end
- end
-
- context 'when experiment active and user in control group' do
- let(:experiment_active) { true }
- let(:in_experiment_group) { false }
-
- it 'shows the empty state control page' do
- expect(page).to have_content('No jobs to show')
- expect(page).to have_link('Get started with Pipelines')
- end
- end
-
- context 'when experiment active and user in experimental group' do
- let(:experiment_active) { true }
- let(:in_experiment_group) { true }
-
- it 'shows the empty state experiment page' do
- expect(page).to have_content('Use jobs to automate your tasks')
- expect(page).to have_link('Create CI/CD configuration file')
- end
+ it 'shows the empty state page' do
+ expect(page).to have_content('Use jobs to automate your tasks')
+ expect(page).to have_link('Create CI/CD configuration file', href: project.present(current_user: user).add_ci_yml_path)
end
end
@@ -102,7 +74,7 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state do
it "shows Finished tab jobs" do
expect(page).to have_selector('.nav-links li.active', text: 'Finished')
- expect(page).to have_content 'No jobs to show'
+ expect(page).to have_content('Use jobs to automate your tasks')
end
end
@@ -533,10 +505,10 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state do
expect(page).to have_content('Trigger token')
expect(page).to have_content('Trigger variables')
- expect(page).not_to have_css('.js-reveal-variables')
+ expect(page).not_to have_selector('[data-testid="trigger-reveal-values-button"]')
- expect(page).to have_selector('.js-build-variable', text: 'TRIGGER_KEY_1')
- expect(page).to have_selector('.js-build-value', text: '••••••')
+ expect(page).to have_selector('[data-testid="trigger-build-key"]', text: 'TRIGGER_KEY_1')
+ expect(page).to have_selector('[data-testid="trigger-build-value"]', text: '••••••')
end
end
@@ -571,17 +543,17 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state do
expect(page).to have_content('Trigger token')
expect(page).to have_content('Trigger variables')
- expect(page).to have_css('.js-reveal-variables')
+ expect(page).to have_selector('[data-testid="trigger-reveal-values-button"]')
- expect(page).to have_selector('.js-build-variable', text: 'TRIGGER_KEY_1')
- expect(page).to have_selector('.js-build-value', text: '••••••')
+ expect(page).to have_selector('[data-testid="trigger-build-key"]', text: 'TRIGGER_KEY_1')
+ expect(page).to have_selector('[data-testid="trigger-build-value"]', text: '••••••')
end
it 'reveals values on button click', :js do
click_button 'Reveal values'
- expect(page).to have_selector('.js-build-variable', text: 'TRIGGER_KEY_1')
- expect(page).to have_selector('.js-build-value', text: 'TRIGGER_VALUE_1')
+ expect(page).to have_selector('[data-testid="trigger-build-key"]', text: 'TRIGGER_KEY_1')
+ expect(page).to have_selector('[data-testid="trigger-build-value"]', text: 'TRIGGER_VALUE_1')
end
end
diff --git a/spec/features/projects/members/anonymous_user_sees_members_spec.rb b/spec/features/projects/members/anonymous_user_sees_members_spec.rb
index 3b0f00c5494..d710ecf6c88 100644
--- a/spec/features/projects/members/anonymous_user_sees_members_spec.rb
+++ b/spec/features/projects/members/anonymous_user_sees_members_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Projects > Members > Anonymous user sees members' do
+ include Spec::Support::Helpers::Features::MembersHelpers
+
let(:user) { create(:user) }
let(:group) { create(:group, :public) }
let(:project) { create(:project, :public) }
@@ -12,11 +14,25 @@ RSpec.describe 'Projects > Members > Anonymous user sees members' do
create(:project_group_link, project: project, group: group)
end
- it "anonymous user visits the project's members page and sees the list of members" do
- visit project_project_members_path(project)
+ context 'when `vue_project_members_list` feature flag is enabled', :js do
+ it "anonymous user visits the project's members page and sees the list of members" do
+ visit project_project_members_path(project)
+
+ expect(find_member_row(user)).to have_content(user.name)
+ end
+ end
+
+ context 'when `vue_project_members_list` feature flag is disabled' do
+ before do
+ stub_feature_flags(vue_project_members_list: false)
+ end
+
+ it "anonymous user visits the project's members page and sees the list of members" do
+ visit project_project_members_path(project)
- expect(current_path).to eq(
- project_project_members_path(project))
- expect(page).to have_content(user.name)
+ expect(current_path).to eq(
+ project_project_members_path(project))
+ expect(page).to have_content(user.name)
+ end
end
end
diff --git a/spec/features/projects/members/group_members_spec.rb b/spec/features/projects/members/group_members_spec.rb
index aa15f04bf24..1abd00421ec 100644
--- a/spec/features/projects/members/group_members_spec.rb
+++ b/spec/features/projects/members/group_members_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Projects members', :js do
+ include Spec::Support::Helpers::Features::MembersHelpers
+
let(:user) { create(:user) }
let(:developer) { create(:user) }
let(:group) { create(:group, :public) }
@@ -18,117 +20,218 @@ RSpec.describe 'Projects members', :js do
sign_in(user)
end
- context 'with a group invitee' do
- before do
- group_invitee
- visit project_project_members_path(project)
- end
+ context 'when `vue_project_members_list` feature flag is enabled' do
+ context 'with a group invitee' do
+ before do
+ group_invitee
+ visit project_project_members_path(project)
+ end
- it 'does not appear in the project members page' do
- page.within first('.content-list') do
- expect(page).not_to have_content('test2@abc.com')
+ it 'does not appear in the project members page' do
+ expect(members_table).not_to have_content('test2@abc.com')
end
end
- end
- context 'with a group' do
- it 'shows group and project members by default' do
- visit project_project_members_path(project)
+ context 'with a group' do
+ it 'shows group and project members by default' do
+ visit project_project_members_path(project)
+
+ expect(members_table).to have_content(developer.name)
+ expect(members_table).to have_content(user.name)
+ expect(members_table).to have_content(group.name)
+ end
+
+ it 'shows project members only if requested' do
+ visit project_project_members_path(project, with_inherited_permissions: 'exclude')
+
+ expect(members_table).to have_content(developer.name)
+ expect(members_table).not_to have_content(user.name)
+ expect(members_table).not_to have_content(group.name)
+ end
- page.within first('.content-list') do
- expect(page).to have_content(developer.name)
+ it 'shows group members only if requested' do
+ visit project_project_members_path(project, with_inherited_permissions: 'only')
- expect(page).to have_content(user.name)
- expect(page).to have_content(group.name)
+ expect(members_table).not_to have_content(developer.name)
+ expect(members_table).to have_content(user.name)
+ expect(members_table).to have_content(group.name)
end
end
- it 'shows project members only if requested' do
- visit project_project_members_path(project, with_inherited_permissions: 'exclude')
+ context 'with a group, a project invitee, and a project requester' do
+ before do
+ group.request_access(group_requester)
+ project.request_access(project_requester)
+ group_invitee
+ project_invitee
+ visit project_project_members_path(project)
+ end
+
+ it 'shows the group owner' do
+ expect(members_table).to have_content(user.name)
+ expect(members_table).to have_content(group.name)
+ end
+
+ it 'shows the project developer' do
+ expect(members_table).to have_content(developer.name)
+ end
+
+ it 'shows the project invitee' do
+ click_link 'Invited'
+
+ expect(members_table).to have_content('test1@abc.com')
+ expect(members_table).not_to have_content('test2@abc.com')
+ end
- page.within first('.content-list') do
- expect(page).to have_content(developer.name)
+ it 'shows the project requester' do
+ click_link 'Access requests'
- expect(page).not_to have_content(user.name)
- expect(page).not_to have_content(group.name)
+ expect(members_table).to have_content(project_requester.name)
+ expect(members_table).not_to have_content(group_requester.name)
end
end
- it 'shows group members only if requested' do
- visit project_project_members_path(project, with_inherited_permissions: 'only')
+ context 'with a group requester' do
+ before do
+ stub_feature_flags(invite_members_group_modal: false)
+ group.request_access(group_requester)
+ visit project_project_members_path(project)
+ end
+
+ it 'does not appear in the project members page' do
+ expect(page).not_to have_link('Access requests')
+ expect(members_table).not_to have_content(group_requester.name)
+ end
+ end
+
+ context 'showing status of members' do
+ it 'shows the status' do
+ create(:user_status, user: user, emoji: 'smirk', message: 'Authoring this object')
- page.within first('.content-list') do
- expect(page).not_to have_content(developer.name)
+ visit project_project_members_path(project)
- expect(page).to have_content(user.name)
- expect(page).to have_content(group.name)
+ expect(first_row).to have_selector('gl-emoji[data-name="smirk"]')
end
end
end
- context 'with a group, a project invitee, and a project requester' do
+ context 'when `vue_project_members_list` feature flag is disabled' do
before do
- group.request_access(group_requester)
- project.request_access(project_requester)
- group_invitee
- project_invitee
- visit project_project_members_path(project)
+ stub_feature_flags(vue_project_members_list: false)
end
- it 'shows the group owner' do
- page.within first('.content-list') do
- # Group owner
- expect(page).to have_content(user.name)
- expect(page).to have_content(group.name)
+ context 'with a group invitee' do
+ before do
+ group_invitee
+ visit project_project_members_path(project)
end
- end
- it 'shows the project developer' do
- page.within first('.content-list') do
- # Project developer
- expect(page).to have_content(developer.name)
+ it 'does not appear in the project members page' do
+ page.within first('.content-list') do
+ expect(page).not_to have_content('test2@abc.com')
+ end
end
end
- it 'shows the project invitee' do
- click_link 'Invited'
+ context 'with a group' do
+ it 'shows group and project members by default' do
+ visit project_project_members_path(project)
- page.within first('.content-list') do
- expect(page).to have_content('test1@abc.com')
- expect(page).not_to have_content('test2@abc.com')
+ page.within first('.content-list') do
+ expect(page).to have_content(developer.name)
+
+ expect(page).to have_content(user.name)
+ expect(page).to have_content(group.name)
+ end
+ end
+
+ it 'shows project members only if requested' do
+ visit project_project_members_path(project, with_inherited_permissions: 'exclude')
+
+ page.within first('.content-list') do
+ expect(page).to have_content(developer.name)
+
+ expect(page).not_to have_content(user.name)
+ expect(page).not_to have_content(group.name)
+ end
end
- end
- it 'shows the project requester' do
- click_link 'Access requests'
+ it 'shows group members only if requested' do
+ visit project_project_members_path(project, with_inherited_permissions: 'only')
- page.within first('.content-list') do
- expect(page).to have_content(project_requester.name)
- expect(page).not_to have_content(group_requester.name)
+ page.within first('.content-list') do
+ expect(page).not_to have_content(developer.name)
+
+ expect(page).to have_content(user.name)
+ expect(page).to have_content(group.name)
+ end
end
end
- end
- context 'with a group requester' do
- before do
- stub_feature_flags(invite_members_group_modal: false)
- group.request_access(group_requester)
- visit project_project_members_path(project)
+ context 'with a group, a project invitee, and a project requester' do
+ before do
+ group.request_access(group_requester)
+ project.request_access(project_requester)
+ group_invitee
+ project_invitee
+ visit project_project_members_path(project)
+ end
+
+ it 'shows the group owner' do
+ page.within first('.content-list') do
+ # Group owner
+ expect(page).to have_content(user.name)
+ expect(page).to have_content(group.name)
+ end
+ end
+
+ it 'shows the project developer' do
+ page.within first('.content-list') do
+ # Project developer
+ expect(page).to have_content(developer.name)
+ end
+ end
+
+ it 'shows the project invitee' do
+ click_link 'Invited'
+
+ page.within first('.content-list') do
+ expect(page).to have_content('test1@abc.com')
+ expect(page).not_to have_content('test2@abc.com')
+ end
+ end
+
+ it 'shows the project requester' do
+ click_link 'Access requests'
+
+ page.within first('.content-list') do
+ expect(page).to have_content(project_requester.name)
+ expect(page).not_to have_content(group_requester.name)
+ end
+ end
end
- it 'does not appear in the project members page' do
- expect(page).not_to have_link('Access requests')
- page.within first('.content-list') do
- expect(page).not_to have_content(group_requester.name)
+ context 'with a group requester' do
+ before do
+ stub_feature_flags(invite_members_group_modal: false)
+ group.request_access(group_requester)
+ visit project_project_members_path(project)
+ end
+
+ it 'does not appear in the project members page' do
+ expect(page).not_to have_link('Access requests')
+ page.within first('.content-list') do
+ expect(page).not_to have_content(group_requester.name)
+ end
end
end
- end
- describe 'showing status of members' do
- it_behaves_like 'showing user status' do
- let(:user_with_status) { developer }
+ context 'showing status of members' do
+ it_behaves_like 'showing user status' do
+ let(:user_with_status) { developer }
- subject { visit project_project_members_path(project) }
+ subject { visit project_project_members_path(project) }
+ end
end
end
end
diff --git a/spec/features/projects/members/groups_with_access_list_spec.rb b/spec/features/projects/members/groups_with_access_list_spec.rb
index 686d86b1783..9d087dfd5f6 100644
--- a/spec/features/projects/members/groups_with_access_list_spec.rb
+++ b/spec/features/projects/members/groups_with_access_list_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Projects > Members > Groups with access list', :js do
+ include Spec::Support::Helpers::Features::MembersHelpers
+
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group, :public) }
let_it_be(:project) { create(:project, :public) }
@@ -15,86 +17,172 @@ RSpec.describe 'Projects > Members > Groups with access list', :js do
project.add_maintainer(user)
sign_in(user)
- visit project_project_members_path(project)
- click_groups_tab
end
- it 'updates group access level' do
- click_button group_link.human_access
-
- page.within '.dropdown-menu' do
- click_link 'Guest'
+ context 'when `vue_project_members_list` feature flag is enabled' do
+ before do
+ visit project_project_members_path(project)
+ click_groups_tab
end
- wait_for_requests
+ it 'updates group access level' do
+ click_button group_link.human_access
+ click_button 'Guest'
- visit project_project_members_path(project)
+ wait_for_requests
- click_groups_tab
+ visit project_project_members_path(project)
- expect(first('.group_member')).to have_content('Guest')
- end
+ click_groups_tab
+
+ expect(find_group_row(group)).to have_content('Guest')
+ end
- it 'updates expiry date' do
- expires_at_field = "member_expires_at_#{group.id}"
- fill_in expires_at_field, with: 3.days.from_now.to_date
+ it 'updates expiry date' do
+ page.within find_group_row(group) do
+ fill_in 'Expiration date', with: 5.days.from_now.to_date
+ find_field('Expiration date').native.send_keys :enter
- find_field(expires_at_field).native.send_keys :enter
- wait_for_requests
+ wait_for_requests
- page.within(find('li.group_member')) do
- expect(page).to have_content('Expires in 3 days')
+ expect(page).to have_content(/in \d days/)
+ end
end
- end
- context 'when link has expiry date set' do
- let(:additional_link_attrs) { { expires_at: 3.days.from_now.to_date } }
+ context 'when link has expiry date set' do
+ let(:additional_link_attrs) { { expires_at: 5.days.from_now.to_date } }
- it 'clears expiry date' do
- page.within(find('li.group_member')) do
- expect(page).to have_content('Expires in 3 days')
+ it 'clears expiry date' do
+ page.within find_group_row(group) do
+ expect(page).to have_content(/in \d days/)
- page.within(find('.js-edit-member-form')) do
- find('.js-clear-input').click
+ find('[data-testid="clear-button"]').click
+
+ wait_for_requests
+
+ expect(page).to have_content('No expiration set')
end
+ end
+ end
- wait_for_requests
+ it 'deletes group link' do
+ expect(page).to have_content(group.full_name)
+
+ page.within find_group_row(group) do
+ click_button 'Remove group'
+ end
- expect(page).not_to have_content('Expires in')
+ page.within('[role="dialog"]') do
+ click_button('Remove group')
+ end
+
+ expect(page).not_to have_content(group.full_name)
+ end
+
+ context 'search in existing members' do
+ it 'finds no results' do
+ fill_in_filtered_search 'Search groups', with: 'testing 123'
+
+ click_groups_tab
+
+ expect(page).not_to have_content(group.full_name)
+ end
+
+ it 'finds results' do
+ fill_in_filtered_search 'Search groups', with: group.full_name
+
+ click_groups_tab
+
+ expect(members_table).to have_content(group.full_name)
end
end
end
- it 'deletes group link' do
- page.within(first('.group_member')) do
- accept_confirm { find('.btn-danger').click }
+ context 'when `vue_project_members_list` feature flag is disabled' do
+ before do
+ stub_feature_flags(vue_project_members_list: false)
+
+ visit project_project_members_path(project)
+ click_groups_tab
end
- wait_for_requests
- expect(page).not_to have_selector('.group_member')
- end
+ it 'updates group access level' do
+ click_button group_link.human_access
- context 'search in existing members' do
- it 'finds no results' do
- page.within '.user-search-form' do
- fill_in 'search_groups', with: 'testing 123'
- find('.user-search-btn').click
+ page.within '.dropdown-menu' do
+ click_link 'Guest'
end
+ wait_for_requests
+
+ visit project_project_members_path(project)
+
click_groups_tab
+ expect(first('.group_member')).to have_content('Guest')
+ end
+
+ it 'updates expiry date' do
+ expires_at_field = "member_expires_at_#{group.id}"
+ fill_in expires_at_field, with: 3.days.from_now.to_date
+
+ find_field(expires_at_field).native.send_keys :enter
+ wait_for_requests
+
+ page.within(find('li.group_member')) do
+ expect(page).to have_content('Expires in 3 days')
+ end
+ end
+
+ context 'when link has expiry date set' do
+ let(:additional_link_attrs) { { expires_at: 3.days.from_now.to_date } }
+
+ it 'clears expiry date' do
+ page.within(find('li.group_member')) do
+ expect(page).to have_content('Expires in 3 days')
+
+ page.within(find('.js-edit-member-form')) do
+ find('.js-clear-input').click
+ end
+
+ wait_for_requests
+
+ expect(page).not_to have_content('Expires in')
+ end
+ end
+ end
+
+ it 'deletes group link' do
+ page.within(first('.group_member')) do
+ accept_confirm { find('.btn-danger').click }
+ end
+ wait_for_requests
+
expect(page).not_to have_selector('.group_member')
end
- it 'finds results' do
- page.within '.user-search-form' do
- fill_in 'search_groups', with: group.name
- find('.user-search-btn').click
+ context 'search in existing members' do
+ it 'finds no results' do
+ page.within '.user-search-form' do
+ fill_in 'search_groups', with: 'testing 123'
+ find('.user-search-btn').click
+ end
+
+ click_groups_tab
+
+ expect(page).not_to have_selector('.group_member')
end
- click_groups_tab
+ it 'finds results' do
+ page.within '.user-search-form' do
+ fill_in 'search_groups', with: group.name
+ find('.user-search-btn').click
+ end
+
+ click_groups_tab
- expect(page).to have_selector('.group_member', count: 1)
+ expect(page).to have_selector('.group_member', count: 1)
+ end
end
end
diff --git a/spec/features/projects/members/invite_group_spec.rb b/spec/features/projects/members/invite_group_spec.rb
index bb56ae348fb..f0d115fef1d 100644
--- a/spec/features/projects/members/invite_group_spec.rb
+++ b/spec/features/projects/members/invite_group_spec.rb
@@ -5,9 +5,14 @@ require 'spec_helper'
RSpec.describe 'Project > Members > Invite group', :js do
include Select2Helper
include ActionView::Helpers::DateHelper
+ include Spec::Support::Helpers::Features::MembersHelpers
let(:maintainer) { create(:user) }
+ before do
+ stub_feature_flags(invite_members_group_modal: false)
+ end
+
describe 'Share with group lock' do
shared_examples 'the project can be shared with groups' do
it 'the "Invite group" tab exists' do
@@ -36,21 +41,45 @@ RSpec.describe 'Project > Members > Invite group', :js do
context 'when the group has "Share with group lock" disabled' do
it_behaves_like 'the project can be shared with groups'
- it 'the project can be shared with another group' do
- visit project_project_members_path(project)
+ context 'when `vue_project_members_list` feature flag is enabled' do
+ it 'the project can be shared with another group' do
+ visit project_project_members_path(project)
- expect(page).not_to have_link 'Groups'
+ expect(page).not_to have_link 'Groups'
- click_on 'invite-group-tab'
+ click_on 'invite-group-tab'
- select2 group_to_share_with.id, from: '#link_group_id'
- page.find('body').click
- find('.btn-success').click
+ select2 group_to_share_with.id, from: '#link_group_id'
+ page.find('body').click
+ find('.btn-success').click
- click_link 'Groups'
+ click_link 'Groups'
- page.within('[data-testid="project-member-groups"]') do
- expect(page).to have_content(group_to_share_with.name)
+ expect(members_table).to have_content(group_to_share_with.name)
+ end
+ end
+
+ context 'when `vue_project_members_list` feature flag is disabled' do
+ before do
+ stub_feature_flags(vue_project_members_list: false)
+ end
+
+ it 'the project can be shared with another group' do
+ visit project_project_members_path(project)
+
+ expect(page).not_to have_link 'Groups'
+
+ click_on 'invite-group-tab'
+
+ select2 group_to_share_with.id, from: '#link_group_id'
+ page.find('body').click
+ find('.btn-success').click
+
+ click_link 'Groups'
+
+ page.within('[data-testid="project-member-groups"]') do
+ expect(page).to have_content(group_to_share_with.name)
+ end
end
end
end
@@ -117,7 +146,7 @@ RSpec.describe 'Project > Members > Invite group', :js do
freeze_time { example.run }
end
- before do
+ def setup
project.add_maintainer(maintainer)
group.add_guest(maintainer)
sign_in(maintainer)
@@ -128,20 +157,37 @@ RSpec.describe 'Project > Members > Invite group', :js do
select2 group.id, from: '#link_group_id'
- fill_in 'expires_at_groups', with: (Time.now + 4.5.days).strftime('%Y-%m-%d')
+ fill_in 'expires_at_groups', with: 5.days.from_now.strftime('%Y-%m-%d')
click_on 'invite-group-tab'
find('.btn-success').click
end
- it 'the group link shows the expiration time with a warning class' do
- click_link 'Groups'
+ context 'when `vue_project_members_list` feature flag is enabled' do
+ it 'the group link shows the expiration time with a warning class' do
+ setup
+ click_link 'Groups'
+
+ expect(find_group_row(group)).to have_content(/in \d days/)
+ expect(find_group_row(group)).to have_selector('.gl-text-orange-500')
+ end
+ end
+
+ context 'when `vue_project_members_list` feature flag is disabled' do
+ before do
+ stub_feature_flags(vue_project_members_list: false)
+ end
- page.within('[data-testid="project-member-groups"]') do
- # Using distance_of_time_in_words_to_now because it is not the same as
- # subtraction, and this way avoids time zone issues as well
- expires_in_text = distance_of_time_in_words_to_now(project.project_group_links.first.expires_at)
- expect(page).to have_content(expires_in_text)
- expect(page).to have_selector('.text-warning')
+ it 'the group link shows the expiration time with a warning class' do
+ setup
+ click_link 'Groups'
+
+ page.within('[data-testid="project-member-groups"]') do
+ # Using distance_of_time_in_words_to_now because it is not the same as
+ # subtraction, and this way avoids time zone issues as well
+ expires_in_text = distance_of_time_in_words_to_now(project.project_group_links.first.expires_at)
+ expect(page).to have_content(expires_in_text)
+ expect(page).to have_selector('.text-warning')
+ end
end
end
end
diff --git a/spec/features/projects/members/list_spec.rb b/spec/features/projects/members/list_spec.rb
index 62115f2dce6..b0fe5b9c48a 100644
--- a/spec/features/projects/members/list_spec.rb
+++ b/spec/features/projects/members/list_spec.rb
@@ -4,7 +4,6 @@ require 'spec_helper'
RSpec.describe 'Project members list' do
include Select2Helper
- include Spec::Support::Helpers::Features::ListRowsHelpers
let(:user1) { create(:user, name: 'John Doe') }
let(:user2) { create(:user, name: 'Mary Jane') }
@@ -13,102 +12,215 @@ RSpec.describe 'Project members list' do
before do
stub_feature_flags(invite_members_group_modal: false)
+
sign_in(user1)
group.add_owner(user1)
end
- it 'show members from project and group' do
- project.add_developer(user2)
+ context 'when `vue_project_members_list` feature flag is enabled', :js do
+ include Spec::Support::Helpers::Features::MembersHelpers
- visit_members_page
+ it 'pushes `vue_project_members_list` feature flag to the frontend' do
+ visit_members_page
- expect(first_row.text).to include(user1.name)
- expect(second_row.text).to include(user2.name)
- end
+ expect(page).to have_pushed_frontend_feature_flags(vueProjectMembersList: true)
+ end
- it 'show user once if member of both group and project' do
- project.add_developer(user1)
+ it 'show members from project and group' do
+ project.add_developer(user2)
- visit_members_page
+ visit_members_page
- expect(first_row.text).to include(user1.name)
- expect(second_row).to be_blank
- end
+ expect(first_row).to have_content(user1.name)
+ expect(second_row).to have_content(user2.name)
+ end
+
+ it 'show user once if member of both group and project' do
+ project.add_developer(user1)
- it 'update user access level', :js do
- project.add_developer(user2)
+ visit_members_page
+
+ expect(first_row).to have_content(user1.name)
+ expect(second_row).to be_blank
+ end
+
+ it 'update user access level', :js do
+ project.add_developer(user2)
- visit_members_page
+ visit_members_page
- page.within(second_row) do
- click_button('Developer')
- click_link('Reporter')
+ page.within find_member_row(user2) do
+ click_button('Developer')
+ click_button('Reporter')
- expect(page).to have_button('Reporter')
+ expect(page).to have_button('Reporter')
+ end
end
- end
- it 'add user to project', :js do
- visit_members_page
+ it 'add user to project', :js do
+ visit_members_page
- add_user(user2.id, 'Reporter')
+ add_user(user2.id, 'Reporter')
- page.within(second_row) do
- expect(page).to have_content(user2.name)
- expect(page).to have_button('Reporter')
+ page.within find_member_row(user2) do
+ expect(page).to have_button('Reporter')
+ end
end
- end
- it 'remove user from project', :js do
- other_user = create(:user)
- project.add_developer(other_user)
+ it 'remove user from project', :js do
+ other_user = create(:user)
+ project.add_developer(other_user)
- visit_members_page
+ visit_members_page
- # Open modal
- find(:css, 'li.project_member', text: other_user.name).find(:css, 'button.btn-danger').click
+ # Open modal
+ page.within find_member_row(other_user) do
+ click_button 'Remove member'
+ end
- expect(page).to have_unchecked_field 'Also unassign this user from related issues and merge requests'
+ page.within('[role="dialog"]') do
+ expect(page).to have_unchecked_field 'Also unassign this user from related issues and merge requests'
+ click_button('Remove member')
+ end
- click_on('Remove member')
+ wait_for_requests
- wait_for_requests
+ expect(members_table).not_to have_content(other_user.name)
+ end
- expect(page).not_to have_content(other_user.name)
- expect(project.users).not_to include(other_user)
- end
+ it 'invite user to project', :js do
+ visit_members_page
+
+ add_user('test@example.com', 'Reporter')
+
+ click_link 'Invited'
+
+ page.within find_invited_member_row('test@example.com') do
+ expect(page).to have_button('Reporter')
+ end
+ end
- it 'invite user to project', :js do
- visit_members_page
+ context 'project bots' do
+ let(:project_bot) { create(:user, :project_bot, name: 'project_bot') }
- add_user('test@example.com', 'Reporter')
+ before do
+ project.add_maintainer(project_bot)
+ end
- click_link 'Invited'
+ it 'does not show form used to change roles and "Expiration date" or the remove user button' do
+ visit_members_page
- page.within(first_row) do
- expect(page).to have_content('test@example.com')
- expect(page).to have_content('Invited')
- expect(page).to have_button('Reporter')
+ page.within find_member_row(project_bot) do
+ expect(page).not_to have_button('Maintainer')
+ expect(page).to have_field('Expiration date', disabled: true)
+ expect(page).not_to have_button('Remove member')
+ end
+ end
end
end
- context 'project bots' do
- let(:project_bot) { create(:user, :project_bot, name: 'project_bot') }
+ context 'when `vue_project_members_list` feature flag is disabled' do
+ include Spec::Support::Helpers::Features::ListRowsHelpers
before do
- project.add_maintainer(project_bot)
+ stub_feature_flags(vue_project_members_list: false)
+ end
+
+ it 'show members from project and group' do
+ project.add_developer(user2)
+
+ visit_members_page
+
+ expect(first_row.text).to include(user1.name)
+ expect(second_row.text).to include(user2.name)
+ end
+
+ it 'show user once if member of both group and project' do
+ project.add_developer(user1)
+
+ visit_members_page
+
+ expect(first_row.text).to include(user1.name)
+ expect(second_row).to be_blank
end
- it 'does not show form used to change roles and "Expiration date" or the remove user button' do
- project_member = project.project_members.find_by(user_id: project_bot.id)
+ it 'update user access level', :js do
+ project.add_developer(user2)
+
+ visit_members_page
+
+ page.within(second_row) do
+ click_button('Developer')
+ click_link('Reporter')
+
+ expect(page).to have_button('Reporter')
+ end
+ end
+ it 'add user to project', :js do
visit_members_page
- expect(page).not_to have_selector("#edit_project_member_#{project_member.id}")
- expect(page).to have_no_selector("#project_member_#{project_member.id} .btn-danger")
+ add_user(user2.id, 'Reporter')
+
+ page.within(second_row) do
+ expect(page).to have_content(user2.name)
+ expect(page).to have_button('Reporter')
+ end
+ end
+
+ it 'remove user from project', :js do
+ other_user = create(:user)
+ project.add_developer(other_user)
+
+ visit_members_page
+
+ # Open modal
+ find(:css, 'li.project_member', text: other_user.name).find(:css, 'button.btn-danger').click
+
+ expect(page).to have_unchecked_field 'Also unassign this user from related issues and merge requests'
+
+ click_on('Remove member')
+
+ wait_for_requests
+
+ expect(page).not_to have_content(other_user.name)
+ expect(project.users).not_to include(other_user)
+ end
+
+ it 'invite user to project', :js do
+ visit_members_page
+
+ add_user('test@example.com', 'Reporter')
+
+ click_link 'Invited'
+
+ page.within(first_row) do
+ expect(page).to have_content('test@example.com')
+ expect(page).to have_content('Invited')
+ expect(page).to have_button('Reporter')
+ end
+ end
+
+ context 'project bots' do
+ let(:project_bot) { create(:user, :project_bot, name: 'project_bot') }
+
+ before do
+ project.add_maintainer(project_bot)
+ end
+
+ it 'does not show form used to change roles and "Expiration date" or the remove user button' do
+ project_member = project.project_members.find_by(user_id: project_bot.id)
+
+ visit_members_page
+
+ expect(page).not_to have_selector("#edit_project_member_#{project_member.id}")
+ expect(page).to have_no_selector("#project_member_#{project_member.id} .btn-danger")
+ end
end
end
+ private
+
def add_user(id, role)
page.within ".invite-users-form" do
select2(id, from: "#user_ids", multiple: true)
diff --git a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
index d69c3f2652c..1127c64e0c7 100644
--- a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
+++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe 'Projects > Members > Maintainer adds member with expiration date', :js do
include Select2Helper
include ActiveSupport::Testing::TimeHelpers
+ include Spec::Support::Helpers::Features::MembersHelpers
let_it_be(:maintainer) { create(:user) }
let_it_be(:project) { create(:project) }
@@ -17,49 +18,107 @@ RSpec.describe 'Projects > Members > Maintainer adds member with expiration date
sign_in(maintainer)
end
- it 'expiration date is displayed in the members list' do
- visit project_project_members_path(project)
+ context 'when `vue_project_members_list` feature flag is enabled' do
+ it 'expiration date is displayed in the members list' do
+ stub_feature_flags(invite_members_group_modal: false)
- page.within '.invite-users-form' do
- select2(new_member.id, from: '#user_ids', multiple: true)
+ visit project_project_members_path(project)
- fill_in 'expires_at', with: 3.days.from_now.to_date
- find_field('expires_at').native.send_keys :enter
+ page.within '.invite-users-form' do
+ select2(new_member.id, from: '#user_ids', multiple: true)
- click_on 'Invite'
+ fill_in 'expires_at', with: 5.days.from_now.to_date
+ find_field('expires_at').native.send_keys :enter
+
+ click_on 'Invite'
+ end
+
+ page.within find_member_row(new_member) do
+ expect(page).to have_content(/in \d days/)
+ end
end
- page.within "#project_member_#{project_member_id}" do
- expect(page).to have_content('Expires in 3 days')
+ it 'changes expiration date' do
+ project.team.add_users([new_member.id], :developer, expires_at: 3.days.from_now.to_date)
+ visit project_project_members_path(project)
+
+ page.within find_member_row(new_member) do
+ fill_in 'Expiration date', with: 5.days.from_now.to_date
+ find_field('Expiration date').native.send_keys :enter
+
+ wait_for_requests
+
+ expect(page).to have_content(/in \d days/)
+ end
end
- end
- it 'changes expiration date' do
- project.team.add_users([new_member.id], :developer, expires_at: Date.today.to_date)
- visit project_project_members_path(project)
+ it 'clears expiration date' do
+ project.team.add_users([new_member.id], :developer, expires_at: 5.days.from_now.to_date)
+ visit project_project_members_path(project)
- page.within "#project_member_#{project_member_id}" do
- fill_in 'Expiration date', with: 3.days.from_now.to_date
- find_field('Expiration date').native.send_keys :enter
+ page.within find_member_row(new_member) do
+ expect(page).to have_content(/in \d days/)
- wait_for_requests
+ find('[data-testid="clear-button"]').click
- expect(page).to have_content('Expires in 3 days')
+ wait_for_requests
+
+ expect(page).to have_content('No expiration set')
+ end
end
end
- it 'clears expiration date' do
- project.team.add_users([new_member.id], :developer, expires_at: 3.days.from_now.to_date)
- visit project_project_members_path(project)
+ context 'when `vue_project_members_list` feature flag is disabled' do
+ before do
+ stub_feature_flags(vue_project_members_list: false)
+ end
+
+ it 'expiration date is displayed in the members list' do
+ stub_feature_flags(invite_members_group_modal: false)
+
+ visit project_project_members_path(project)
+
+ page.within '.invite-users-form' do
+ select2(new_member.id, from: '#user_ids', multiple: true)
+
+ fill_in 'expires_at', with: 3.days.from_now.to_date
+ find_field('expires_at').native.send_keys :enter
+
+ click_on 'Invite'
+ end
+
+ page.within "#project_member_#{project_member_id}" do
+ expect(page).to have_content('Expires in 3 days')
+ end
+ end
+
+ it 'changes expiration date' do
+ project.team.add_users([new_member.id], :developer, expires_at: 1.day.from_now.to_date)
+ visit project_project_members_path(project)
+
+ page.within "#project_member_#{project_member_id}" do
+ fill_in 'Expiration date', with: 3.days.from_now.to_date
+ find_field('Expiration date').native.send_keys :enter
+
+ wait_for_requests
+
+ expect(page).to have_content('Expires in 3 days')
+ end
+ end
+
+ it 'clears expiration date' do
+ project.team.add_users([new_member.id], :developer, expires_at: 3.days.from_now.to_date)
+ visit project_project_members_path(project)
- page.within "#project_member_#{project_member_id}" do
- expect(page).to have_content('Expires in 3 days')
+ page.within "#project_member_#{project_member_id}" do
+ expect(page).to have_content('Expires in 3 days')
- find('.js-clear-input').click
+ find('.js-clear-input').click
- wait_for_requests
+ wait_for_requests
- expect(page).not_to have_content('Expires in')
+ expect(page).not_to have_content('Expires in')
+ end
end
end
diff --git a/spec/features/projects/members/sorting_spec.rb b/spec/features/projects/members/sorting_spec.rb
index be27cbc0d66..3c132747bc4 100644
--- a/spec/features/projects/members/sorting_spec.rb
+++ b/spec/features/projects/members/sorting_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Projects > Members > Sorting' do
+ include Spec::Support::Helpers::Features::MembersHelpers
+
let(:maintainer) { create(:user, name: 'John Doe') }
let(:developer) { create(:user, name: 'Mary Jane', last_sign_in_at: 5.days.ago) }
let(:project) { create(:project, namespace: maintainer.namespace, creator: maintainer) }
@@ -13,78 +15,169 @@ RSpec.describe 'Projects > Members > Sorting' do
sign_in(maintainer)
end
- it 'sorts alphabetically by default' do
- visit_members_list(sort: nil)
+ context 'when `vue_project_members_list` feature flag is enabled', :js do
+ it 'sorts by account by default' do
+ visit_members_list(sort: nil)
- expect(first_member).to include(maintainer.name)
- expect(second_member).to include(developer.name)
- expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
- end
+ expect(first_row).to have_content(maintainer.name)
+ expect(second_row).to have_content(developer.name)
- it 'sorts by access level ascending' do
- visit_members_list(sort: :access_level_asc)
+ expect_sort_by('Account', :asc)
+ end
- expect(first_member).to include(developer.name)
- expect(second_member).to include(maintainer.name)
- expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Access level, ascending')
- end
+ it 'sorts by max role ascending' do
+ visit_members_list(sort: :access_level_asc)
- it 'sorts by access level descending' do
- visit_members_list(sort: :access_level_desc)
+ expect(first_row).to have_content(developer.name)
+ expect(second_row).to have_content(maintainer.name)
- expect(first_member).to include(maintainer.name)
- expect(second_member).to include(developer.name)
- expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Access level, descending')
- end
+ expect_sort_by('Max role', :asc)
+ end
- it 'sorts by last joined' do
- visit_members_list(sort: :last_joined)
+ it 'sorts by max role descending' do
+ visit_members_list(sort: :access_level_desc)
- expect(first_member).to include(maintainer.name)
- expect(second_member).to include(developer.name)
- expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Last joined')
- end
+ expect(first_row).to have_content(maintainer.name)
+ expect(second_row).to have_content(developer.name)
- it 'sorts by oldest joined' do
- visit_members_list(sort: :oldest_joined)
+ expect_sort_by('Max role', :desc)
+ end
- expect(first_member).to include(developer.name)
- expect(second_member).to include(maintainer.name)
- expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Oldest joined')
- end
+ it 'sorts by access granted ascending' do
+ visit_members_list(sort: :last_joined)
- it 'sorts by name ascending' do
- visit_members_list(sort: :name_asc)
+ expect(first_row).to have_content(maintainer.name)
+ expect(second_row).to have_content(developer.name)
- expect(first_member).to include(maintainer.name)
- expect(second_member).to include(developer.name)
- expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
- end
+ expect_sort_by('Access granted', :asc)
+ end
- it 'sorts by name descending' do
- visit_members_list(sort: :name_desc)
+ it 'sorts by access granted descending' do
+ visit_members_list(sort: :oldest_joined)
- expect(first_member).to include(developer.name)
- expect(second_member).to include(maintainer.name)
- expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Name, descending')
- end
+ expect(first_row).to have_content(developer.name)
+ expect(second_row).to have_content(maintainer.name)
+
+ expect_sort_by('Access granted', :desc)
+ end
+
+ it 'sorts by account ascending' do
+ visit_members_list(sort: :name_asc)
- it 'sorts by recent sign in', :clean_gitlab_redis_shared_state do
- visit_members_list(sort: :recent_sign_in)
+ expect(first_row).to have_content(maintainer.name)
+ expect(second_row).to have_content(developer.name)
- expect(first_member).to include(maintainer.name)
- expect(second_member).to include(developer.name)
- expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Recent sign in')
+ expect_sort_by('Account', :asc)
+ end
+
+ it 'sorts by account descending' do
+ visit_members_list(sort: :name_desc)
+
+ expect(first_row).to have_content(developer.name)
+ expect(second_row).to have_content(maintainer.name)
+
+ expect_sort_by('Account', :desc)
+ end
+
+ it 'sorts by last sign-in ascending', :clean_gitlab_redis_shared_state do
+ visit_members_list(sort: :recent_sign_in)
+
+ expect(first_row).to have_content(maintainer.name)
+ expect(second_row).to have_content(developer.name)
+
+ expect_sort_by('Last sign-in', :asc)
+ end
+
+ it 'sorts by last sign-in descending', :clean_gitlab_redis_shared_state do
+ visit_members_list(sort: :oldest_sign_in)
+
+ expect(first_row).to have_content(developer.name)
+ expect(second_row).to have_content(maintainer.name)
+
+ expect_sort_by('Last sign-in', :desc)
+ end
end
- it 'sorts by oldest sign in', :clean_gitlab_redis_shared_state do
- visit_members_list(sort: :oldest_sign_in)
+ context 'when `vue_project_members_list` feature flag is disabled' do
+ before do
+ stub_feature_flags(vue_project_members_list: false)
+ end
+
+ it 'sorts alphabetically by default' do
+ visit_members_list(sort: nil)
- expect(first_member).to include(developer.name)
- expect(second_member).to include(maintainer.name)
- expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Oldest sign in')
+ expect(first_member).to include(maintainer.name)
+ expect(second_member).to include(developer.name)
+ expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
+ end
+
+ it 'sorts by access level ascending' do
+ visit_members_list(sort: :access_level_asc)
+
+ expect(first_member).to include(developer.name)
+ expect(second_member).to include(maintainer.name)
+ expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Access level, ascending')
+ end
+
+ it 'sorts by access level descending' do
+ visit_members_list(sort: :access_level_desc)
+
+ expect(first_member).to include(maintainer.name)
+ expect(second_member).to include(developer.name)
+ expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Access level, descending')
+ end
+
+ it 'sorts by last joined' do
+ visit_members_list(sort: :last_joined)
+
+ expect(first_member).to include(maintainer.name)
+ expect(second_member).to include(developer.name)
+ expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Last joined')
+ end
+
+ it 'sorts by oldest joined' do
+ visit_members_list(sort: :oldest_joined)
+
+ expect(first_member).to include(developer.name)
+ expect(second_member).to include(maintainer.name)
+ expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Oldest joined')
+ end
+
+ it 'sorts by name ascending' do
+ visit_members_list(sort: :name_asc)
+
+ expect(first_member).to include(maintainer.name)
+ expect(second_member).to include(developer.name)
+ expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
+ end
+
+ it 'sorts by name descending' do
+ visit_members_list(sort: :name_desc)
+
+ expect(first_member).to include(developer.name)
+ expect(second_member).to include(maintainer.name)
+ expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Name, descending')
+ end
+
+ it 'sorts by recent sign in', :clean_gitlab_redis_shared_state do
+ visit_members_list(sort: :recent_sign_in)
+
+ expect(first_member).to include(maintainer.name)
+ expect(second_member).to include(developer.name)
+ expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Recent sign in')
+ end
+
+ it 'sorts by oldest sign in', :clean_gitlab_redis_shared_state do
+ visit_members_list(sort: :oldest_sign_in)
+
+ expect(first_member).to include(developer.name)
+ expect(second_member).to include(maintainer.name)
+ expect(page).to have_css('.qa-user-sort-dropdown .dropdown-toggle-text', text: 'Oldest sign in')
+ end
end
+ private
+
def visit_members_list(sort:)
visit project_project_members_path(project, sort: sort)
end
@@ -96,4 +189,11 @@ RSpec.describe 'Projects > Members > Sorting' do
def second_member
page.all('ul.content-list > li').last.text
end
+
+ def expect_sort_by(text, sort_direction)
+ within('[data-testid="members-sort-dropdown"]') do
+ expect(page).to have_css('button[aria-haspopup="true"]', text: text)
+ expect(page).to have_button("Sorting Direction: #{sort_direction == :asc ? 'Ascending' : 'Descending'}")
+ end
+ end
end
diff --git a/spec/features/projects/members/tabs_spec.rb b/spec/features/projects/members/tabs_spec.rb
index bdcf02c82a4..eef3395de91 100644
--- a/spec/features/projects/members/tabs_spec.rb
+++ b/spec/features/projects/members/tabs_spec.rb
@@ -3,6 +3,7 @@
require 'spec_helper'
RSpec.describe 'Projects > Members > Tabs' do
+ include Spec::Support::Helpers::Features::MembersHelpers
using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) }
@@ -19,55 +20,93 @@ RSpec.describe 'Projects > Members > Tabs' do
end
end
- before do
- allow(Kaminari.config).to receive(:default_per_page).and_return(1)
-
- sign_in(user)
- visit project_project_members_path(project)
- end
+ context 'tabs' do
+ before do
+ sign_in(user)
+ visit project_project_members_path(project)
+ end
- where(:tab, :count) do
- 'Members' | 3
- 'Invited' | 2
- 'Groups' | 2
- 'Access requests' | 2
- end
+ where(:tab, :count) do
+ 'Members' | 3
+ 'Invited' | 2
+ 'Groups' | 2
+ 'Access requests' | 2
+ end
- with_them do
- it "renders #{params[:tab]} tab" do
- expect(page).to have_selector('.nav-link', text: "#{tab} #{count}")
+ with_them do
+ it "renders #{params[:tab]} tab" do
+ expect(page).to have_selector('.nav-link', text: "#{tab} #{count}")
+ end
end
- end
- context 'displays "Members" tab by default' do
- it_behaves_like 'active "Members" tab'
+ context 'displays "Members" tab by default' do
+ it_behaves_like 'active "Members" tab'
+ end
end
- context 'when searching "Groups"', :js do
+ context 'when `vue_project_members_list` feature flag is enabled' do
before do
- click_link 'Groups'
+ sign_in(user)
+ visit project_project_members_path(project)
+ end
+
+ context 'when searching "Groups"', :js do
+ before do
+ click_link 'Groups'
+
+ fill_in_filtered_search 'Search groups', with: 'group'
+ end
- page.within '[data-testid="group-link-search-form"]' do
- fill_in 'search_groups', with: 'group'
- find('button[type="submit"]').click
+ it 'displays "Groups" tab' do
+ expect(page).to have_selector('.nav-link.active', text: 'Groups')
+ end
+
+ context 'and then searching "Members"' do
+ before do
+ click_link 'Members 3'
+
+ fill_in_filtered_search 'Filter members', with: 'user'
+ end
+
+ it_behaves_like 'active "Members" tab'
end
end
+ end
+
+ context 'when `vue_project_members_list` feature flag is disabled' do
+ before do
+ stub_feature_flags(vue_project_members_list: false)
- it 'displays "Groups" tab' do
- expect(page).to have_selector('.nav-link.active', text: 'Groups')
+ sign_in(user)
+ visit project_project_members_path(project)
end
- context 'and then searching "Members"' do
+ context 'when searching "Groups"', :js do
before do
- click_link 'Members 3'
+ click_link 'Groups'
- page.within '[data-testid="user-search-form"]' do
- fill_in 'search', with: 'user'
+ page.within '[data-testid="group-link-search-form"]' do
+ fill_in 'search_groups', with: 'group'
find('button[type="submit"]').click
end
end
- it_behaves_like 'active "Members" tab'
+ it 'displays "Groups" tab' do
+ expect(page).to have_selector('.nav-link.active', text: 'Groups')
+ end
+
+ context 'and then searching "Members"' do
+ before do
+ click_link 'Members 3'
+
+ page.within '[data-testid="user-search-form"]' do
+ fill_in 'search', with: 'user'
+ find('button[type="submit"]').click
+ end
+ end
+
+ it_behaves_like 'active "Members" tab'
+ end
end
end
end
diff --git a/spec/features/projects/navbar_spec.rb b/spec/features/projects/navbar_spec.rb
index 25791b393bc..4ff3827b240 100644
--- a/spec/features/projects/navbar_spec.rb
+++ b/spec/features/projects/navbar_spec.rb
@@ -67,23 +67,4 @@ RSpec.describe 'Project navbar' do
it_behaves_like 'verified navigation bar'
end
-
- context 'when invite team members is not available' do
- it 'does not display the js-invite-members-trigger' do
- visit project_path(project)
-
- expect(page).not_to have_selector('.js-invite-members-trigger')
- end
- end
-
- context 'when invite team members is available' do
- it 'includes the div for js-invite-members-trigger' do
- stub_feature_flags(invite_members_group_modal: true)
- allow_any_instance_of(InviteMembersHelper).to receive(:invite_members_allowed?).and_return(true)
-
- visit project_path(project)
-
- expect(page).to have_selector('.js-invite-members-trigger')
- end
- end
end
diff --git a/spec/features/projects/pages/user_adds_domain_spec.rb b/spec/features/projects/pages/user_adds_domain_spec.rb
new file mode 100644
index 00000000000..24c9edb79e5
--- /dev/null
+++ b/spec/features/projects/pages/user_adds_domain_spec.rb
@@ -0,0 +1,185 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe 'User adds pages domain', :js do
+ include LetsEncryptHelpers
+
+ let_it_be(:project) { create(:project, pages_https_only: false) }
+ let(:user) { create(:user) }
+
+ before do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+
+ project.add_maintainer(user)
+
+ sign_in(user)
+ end
+
+ context 'when pages are exposed on external HTTP address', :http_pages_enabled do
+ let(:project) { create(:project, pages_https_only: false) }
+
+ shared_examples 'adds new domain' do
+ it 'adds new domain' do
+ visit new_project_pages_domain_path(project)
+
+ fill_in 'Domain', with: 'my.test.domain.com'
+ click_button 'Create New Domain'
+
+ expect(page).to have_content('my.test.domain.com')
+ end
+ end
+
+ it 'allows to add new domain' do
+ visit project_pages_path(project)
+
+ expect(page).to have_content('New Domain')
+ end
+
+ it_behaves_like 'adds new domain'
+
+ context 'when project in group namespace' do
+ it_behaves_like 'adds new domain' do
+ let(:group) { create :group }
+ let(:project) { create(:project, namespace: group, pages_https_only: false) }
+ end
+ end
+
+ context 'when pages domain is added' do
+ before do
+ create(:pages_domain, project: project, domain: 'my.test.domain.com')
+
+ visit new_project_pages_domain_path(project)
+ end
+
+ it 'renders certificates is disabled' do
+ expect(page).to have_content('Support for custom certificates is disabled')
+ end
+
+ it 'does not adds new domain and renders error message' do
+ fill_in 'Domain', with: 'my.test.domain.com'
+ click_button 'Create New Domain'
+
+ expect(page).to have_content('Domain has already been taken')
+ end
+ end
+ end
+
+ context 'when pages are exposed on external HTTPS address', :https_pages_enabled, :js do
+ let(:certificate_pem) do
+ attributes_for(:pages_domain)[:certificate]
+ end
+
+ let(:certificate_key) do
+ attributes_for(:pages_domain)[:key]
+ end
+
+ it 'adds new domain with certificate' do
+ visit new_project_pages_domain_path(project)
+
+ fill_in 'Domain', with: 'my.test.domain.com'
+
+ fill_in 'Certificate (PEM)', with: certificate_pem
+ fill_in 'Key (PEM)', with: certificate_key
+ click_button 'Create New Domain'
+
+ expect(page).to have_content('my.test.domain.com')
+ end
+
+ it "adds new domain with certificate if Let's Encrypt is enabled" do
+ stub_lets_encrypt_settings
+
+ visit new_project_pages_domain_path(project)
+
+ fill_in 'Domain', with: 'my.test.domain.com'
+
+ find('.js-auto-ssl-toggle-container .project-feature-toggle').click
+
+ fill_in 'Certificate (PEM)', with: certificate_pem
+ fill_in 'Key (PEM)', with: certificate_key
+ click_button 'Create New Domain'
+
+ expect(page).to have_content('my.test.domain.com')
+ end
+
+ it 'shows validation error if domain is duplicated' do
+ project.pages_domains.create!(domain: 'my.test.domain.com')
+
+ visit new_project_pages_domain_path(project)
+
+ fill_in 'Domain', with: 'my.test.domain.com'
+ click_button 'Create New Domain'
+
+ expect(page).to have_content('Domain has already been taken')
+ end
+
+ describe 'with dns verification enabled' do
+ before do
+ stub_application_setting(pages_domain_verification_enabled: true)
+ end
+
+ it 'shows the DNS verification record' do
+ domain = create(:pages_domain, project: project)
+
+ visit project_pages_path(project)
+
+ within('#content-body') { click_link 'Edit' }
+ expect(page).to have_field :domain_verification, with: "#{domain.verification_domain} TXT #{domain.keyed_verification_code}"
+ end
+ end
+
+ describe 'updating the certificate for an existing domain' do
+ let!(:domain) do
+ create(:pages_domain, project: project, auto_ssl_enabled: false)
+ end
+
+ it 'allows the certificate to be updated' do
+ visit project_pages_path(project)
+
+ within('#content-body') { click_link 'Edit' }
+ click_button 'Save Changes'
+
+ expect(page).to have_content('Domain was updated')
+ end
+
+ context 'when the certificate is invalid' do
+ let!(:domain) do
+ create(:pages_domain, :without_certificate, :without_key, project: project)
+ end
+
+ it 'tells the user what the problem is' do
+ visit project_pages_path(project)
+
+ within('#content-body') { click_link 'Edit' }
+
+ fill_in 'Certificate (PEM)', with: 'invalid data'
+ click_button 'Save Changes'
+
+ expect(page).to have_content('Certificate must be a valid PEM certificate')
+ expect(page).to have_content('Certificate misses intermediates')
+ expect(page).to have_content("Key doesn't match the certificate")
+ end
+ end
+
+ it 'allows the certificate to be removed', :js do
+ visit project_pages_path(project)
+
+ within('#content-body') { click_link 'Edit' }
+
+ accept_confirm { click_link 'Remove' }
+
+ expect(page).to have_field('Certificate (PEM)', with: '')
+ expect(page).to have_field('Key (PEM)', with: '')
+ domain.reload
+ expect(domain.certificate).to be_nil
+ expect(domain.key).to be_nil
+ end
+
+ it 'shows the DNS CNAME record' do
+ visit project_pages_path(project)
+
+ within('#content-body') { click_link 'Edit' }
+ expect(page).to have_field :domain_dns, with: "#{domain.domain} CNAME #{domain.project.pages_subdomain}.#{Settings.pages.host}."
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/pages_lets_encrypt_spec.rb b/spec/features/projects/pages/user_edits_lets_encrypt_settings_spec.rb
index 302e9f5e533..cf8438d5e6f 100644
--- a/spec/features/projects/pages_lets_encrypt_spec.rb
+++ b/spec/features/projects/pages/user_edits_lets_encrypt_settings_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe "Pages with Let's Encrypt", :https_pages_enabled do
project.add_role(user, role)
sign_in(user)
- project.namespace.update(owner: user)
+ project.namespace.update!(owner: user)
allow_next_instance_of(Project) do |instance|
allow(instance).to receive(:pages_deployed?) { true }
end
diff --git a/spec/features/projects/pages/user_edits_settings_spec.rb b/spec/features/projects/pages/user_edits_settings_spec.rb
new file mode 100644
index 00000000000..3649fae17ce
--- /dev/null
+++ b/spec/features/projects/pages/user_edits_settings_spec.rb
@@ -0,0 +1,201 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe 'Pages edits pages settings', :js do
+ let(:project) { create(:project, pages_https_only: false) }
+ let(:user) { create(:user) }
+
+ before do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+
+ project.add_maintainer(user)
+
+ sign_in(user)
+ end
+
+ context 'when user is the owner' do
+ before do
+ project.namespace.update!(owner: user)
+ end
+
+ context 'when pages deployed' do
+ before do
+ project.mark_pages_as_deployed
+ end
+
+ it 'renders Access pages' do
+ visit project_pages_path(project)
+
+ expect(page).to have_content('Access pages')
+ end
+
+ context 'when pages are disabled in the project settings' do
+ it 'renders disabled warning' do
+ project.project_feature.update!(pages_access_level: ProjectFeature::DISABLED)
+
+ visit project_pages_path(project)
+
+ expect(page).to have_content('GitLab Pages are disabled for this project')
+ end
+ end
+
+ it 'renders first deployment warning' do
+ visit project_pages_path(project)
+
+ expect(page).to have_content('It may take up to 30 minutes before the site is available after the first deployment.')
+ end
+
+ shared_examples 'does not render access control warning' do
+ it 'does not render access control warning' do
+ visit project_pages_path(project)
+
+ expect(page).not_to have_content('Access Control is enabled for this Pages website')
+ end
+ end
+
+ include_examples 'does not render access control warning'
+
+ context 'when access control is enabled in gitlab settings' do
+ before do
+ stub_pages_setting(access_control: true)
+ end
+
+ it 'renders access control warning' do
+ visit project_pages_path(project)
+
+ expect(page).to have_content('Access Control is enabled for this Pages website')
+ end
+
+ context 'when pages are public' do
+ before do
+ project.project_feature.update!(pages_access_level: ProjectFeature::PUBLIC)
+ end
+
+ include_examples 'does not render access control warning'
+ end
+ end
+
+ context 'when support for external domains is disabled' do
+ it 'renders message that support is disabled' do
+ visit project_pages_path(project)
+
+ expect(page).to have_content('Support for domains and certificates is disabled')
+ end
+ end
+ end
+
+ it 'does not see anything to destroy' do
+ visit project_pages_path(project)
+
+ expect(page).to have_content('Configure pages')
+ expect(page).not_to have_link('Remove pages')
+ end
+
+ describe 'project settings page' do
+ it 'renders "Pages" tab' do
+ visit edit_project_path(project)
+
+ page.within '.nav-sidebar' do
+ expect(page).to have_link('Pages')
+ end
+ end
+
+ context 'when pages are disabled' do
+ before do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(false)
+ end
+
+ it 'does not render "Pages" tab' do
+ visit edit_project_path(project)
+
+ page.within '.nav-sidebar' do
+ expect(page).not_to have_link('Pages')
+ end
+ end
+ end
+ end
+ end
+
+ describe 'HTTPS settings', :https_pages_enabled do
+ before do
+ project.namespace.update!(owner: user)
+
+ project.mark_pages_as_deployed
+ end
+
+ it 'tries to change the setting' do
+ visit project_pages_path(project)
+ expect(page).to have_content("Force HTTPS (requires valid certificates)")
+
+ uncheck :project_pages_https_only
+
+ click_button 'Save'
+
+ expect(page).to have_text('Your changes have been saved')
+ expect(page).not_to have_checked_field('project_pages_https_only')
+ end
+
+ context 'setting could not be updated' do
+ let(:service) { instance_double('Projects::UpdateService') }
+
+ before do
+ allow(Projects::UpdateService).to receive(:new).and_return(service)
+ allow(service).to receive(:execute).and_return(status: :error, message: 'Some error has occured')
+ end
+
+ it 'tries to change the setting' do
+ visit project_pages_path(project)
+
+ uncheck :project_pages_https_only
+
+ click_button 'Save'
+
+ expect(page).to have_text('Some error has occured')
+ end
+ end
+
+ context 'non-HTTPS domain exists' do
+ let(:project) { create(:project, pages_https_only: false) }
+
+ before do
+ create(:pages_domain, :without_key, :without_certificate, project: project)
+ end
+
+ it 'the setting is disabled' do
+ visit project_pages_path(project)
+
+ expect(page).to have_field(:project_pages_https_only, disabled: true)
+ expect(page).to have_button('Save')
+ end
+ end
+
+ context 'HTTPS pages are disabled', :https_pages_disabled do
+ it 'the setting is unavailable' do
+ visit project_pages_path(project)
+
+ expect(page).not_to have_field(:project_pages_https_only)
+ expect(page).not_to have_content('Force HTTPS (requires valid certificates)')
+ expect(page).to have_button('Save')
+ end
+ end
+ end
+
+ describe 'Remove page' do
+ context 'when pages are deployed' do
+ before do
+ project.mark_pages_as_deployed
+ end
+
+ it 'removes the pages', :sidekiq_inline do
+ visit project_pages_path(project)
+
+ expect(page).to have_link('Remove pages')
+
+ accept_confirm { click_link 'Remove pages' }
+
+ expect(page).to have_content('Pages were scheduled for removal')
+ expect(project.reload.pages_deployed?).to be_falsey
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/pages_spec.rb b/spec/features/projects/pages_spec.rb
deleted file mode 100644
index 11f712fde81..00000000000
--- a/spec/features/projects/pages_spec.rb
+++ /dev/null
@@ -1,411 +0,0 @@
-# frozen_string_literal: true
-require 'spec_helper'
-
-RSpec.shared_examples 'pages settings editing' do
- let_it_be(:project) { create(:project, pages_https_only: false) }
- let(:user) { create(:user) }
- let(:role) { :maintainer }
-
- before do
- allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
-
- project.add_role(user, role)
-
- sign_in(user)
- end
-
- context 'when user is the owner' do
- before do
- project.namespace.update(owner: user)
- end
-
- context 'when pages deployed' do
- before do
- allow_any_instance_of(Project).to receive(:pages_deployed?) { true }
- end
-
- it 'renders Access pages' do
- visit project_pages_path(project)
-
- expect(page).to have_content('Access pages')
- end
-
- context 'when pages are disabled in the project settings' do
- it 'renders disabled warning' do
- project.project_feature.update!(pages_access_level: ProjectFeature::DISABLED)
-
- visit project_pages_path(project)
-
- expect(page).to have_content('GitLab Pages are disabled for this project')
- end
- end
-
- it 'renders first deployment warning' do
- visit project_pages_path(project)
-
- expect(page).to have_content('It may take up to 30 minutes before the site is available after the first deployment.')
- end
-
- shared_examples 'does not render access control warning' do
- it 'does not render access control warning' do
- visit project_pages_path(project)
-
- expect(page).not_to have_content('Access Control is enabled for this Pages website')
- end
- end
-
- include_examples 'does not render access control warning'
-
- context 'when access control is enabled in gitlab settings' do
- before do
- stub_pages_setting(access_control: true)
- end
-
- it 'renders access control warning' do
- visit project_pages_path(project)
-
- expect(page).to have_content('Access Control is enabled for this Pages website')
- end
-
- context 'when pages are public' do
- before do
- project.project_feature.update!(pages_access_level: ProjectFeature::PUBLIC)
- end
-
- include_examples 'does not render access control warning'
- end
- end
-
- context 'when support for external domains is disabled' do
- it 'renders message that support is disabled' do
- visit project_pages_path(project)
-
- expect(page).to have_content('Support for domains and certificates is disabled')
- end
- end
-
- context 'when pages are exposed on external HTTP address', :http_pages_enabled do
- let(:project) { create(:project, pages_https_only: false) }
-
- shared_examples 'adds new domain' do
- it 'adds new domain' do
- visit new_project_pages_domain_path(project)
-
- fill_in 'Domain', with: 'my.test.domain.com'
- click_button 'Create New Domain'
-
- expect(page).to have_content('my.test.domain.com')
- end
- end
-
- it 'allows to add new domain' do
- visit project_pages_path(project)
-
- expect(page).to have_content('New Domain')
- end
-
- it_behaves_like 'adds new domain'
-
- context 'when project in group namespace' do
- it_behaves_like 'adds new domain' do
- let(:group) { create :group }
- let(:project) { create(:project, namespace: group, pages_https_only: false) }
- end
- end
-
- context 'when pages domain is added' do
- before do
- create(:pages_domain, project: project, domain: 'my.test.domain.com')
-
- visit new_project_pages_domain_path(project)
- end
-
- it 'renders certificates is disabled' do
- expect(page).to have_content('Support for custom certificates is disabled')
- end
-
- it 'does not adds new domain and renders error message' do
- fill_in 'Domain', with: 'my.test.domain.com'
- click_button 'Create New Domain'
-
- expect(page).to have_content('Domain has already been taken')
- end
- end
- end
-
- context 'when pages are exposed on external HTTPS address', :https_pages_enabled, :js do
- let(:certificate_pem) do
- attributes_for(:pages_domain)[:certificate]
- end
-
- let(:certificate_key) do
- attributes_for(:pages_domain)[:key]
- end
-
- it 'adds new domain with certificate' do
- visit new_project_pages_domain_path(project)
-
- fill_in 'Domain', with: 'my.test.domain.com'
-
- if ::Gitlab::LetsEncrypt.enabled?
- find('.js-auto-ssl-toggle-container .project-feature-toggle').click
- end
-
- fill_in 'Certificate (PEM)', with: certificate_pem
- fill_in 'Key (PEM)', with: certificate_key
- click_button 'Create New Domain'
-
- expect(page).to have_content('my.test.domain.com')
- end
-
- it 'shows validation error if domain is duplicated' do
- project.pages_domains.create!(domain: 'my.test.domain.com')
-
- visit new_project_pages_domain_path(project)
-
- fill_in 'Domain', with: 'my.test.domain.com'
- click_button 'Create New Domain'
-
- expect(page).to have_content('Domain has already been taken')
- end
-
- describe 'with dns verification enabled' do
- before do
- stub_application_setting(pages_domain_verification_enabled: true)
- end
-
- it 'shows the DNS verification record' do
- domain = create(:pages_domain, project: project)
-
- visit project_pages_path(project)
-
- within('#content-body') { click_link 'Edit' }
- expect(page).to have_field :domain_verification, with: "#{domain.verification_domain} TXT #{domain.keyed_verification_code}"
- end
- end
-
- describe 'updating the certificate for an existing domain' do
- let!(:domain) do
- create(:pages_domain, project: project, auto_ssl_enabled: false)
- end
-
- it 'allows the certificate to be updated' do
- visit project_pages_path(project)
-
- within('#content-body') { click_link 'Edit' }
- click_button 'Save Changes'
-
- expect(page).to have_content('Domain was updated')
- end
-
- context 'when the certificate is invalid' do
- let!(:domain) do
- create(:pages_domain, :without_certificate, :without_key, project: project)
- end
-
- it 'tells the user what the problem is' do
- visit project_pages_path(project)
-
- within('#content-body') { click_link 'Edit' }
-
- if ::Gitlab::LetsEncrypt.enabled?
- find('.js-auto-ssl-toggle-container .project-feature-toggle').click
- end
-
- fill_in 'Certificate (PEM)', with: 'invalid data'
- click_button 'Save Changes'
-
- expect(page).to have_content('Certificate must be a valid PEM certificate')
- expect(page).to have_content('Certificate misses intermediates')
- expect(page).to have_content("Key doesn't match the certificate")
- end
- end
-
- it 'allows the certificate to be removed', :js do
- visit project_pages_path(project)
-
- within('#content-body') { click_link 'Edit' }
-
- accept_confirm { click_link 'Remove' }
-
- expect(page).to have_field('Certificate (PEM)', with: '')
- expect(page).to have_field('Key (PEM)', with: '')
- domain.reload
- expect(domain.certificate).to be_nil
- expect(domain.key).to be_nil
- end
-
- it 'shows the DNS CNAME record' do
- visit project_pages_path(project)
-
- within('#content-body') { click_link 'Edit' }
- expect(page).to have_field :domain_dns, with: "#{domain.domain} CNAME #{domain.project.pages_subdomain}.#{Settings.pages.host}."
- end
- end
- end
- end
-
- it 'does not see anything to destroy' do
- visit project_pages_path(project)
-
- expect(page).to have_content('Configure pages')
- expect(page).not_to have_link('Remove pages')
- end
-
- describe 'project settings page' do
- it 'renders "Pages" tab' do
- visit edit_project_path(project)
-
- page.within '.nav-sidebar' do
- expect(page).to have_link('Pages')
- end
- end
-
- context 'when pages are disabled' do
- before do
- allow(Gitlab.config.pages).to receive(:enabled).and_return(false)
- end
-
- it 'does not render "Pages" tab' do
- visit edit_project_path(project)
-
- page.within '.nav-sidebar' do
- expect(page).not_to have_link('Pages')
- end
- end
- end
- end
- end
-
- describe 'HTTPS settings', :https_pages_enabled do
- before do
- project.namespace.update(owner: user)
-
- allow_any_instance_of(Project).to receive(:pages_deployed?) { true }
- end
-
- it 'tries to change the setting' do
- visit project_pages_path(project)
- expect(page).to have_content("Force HTTPS (requires valid certificates)")
-
- uncheck :project_pages_https_only
-
- click_button 'Save'
-
- expect(page).to have_text('Your changes have been saved')
- expect(page).not_to have_checked_field('project_pages_https_only')
- end
-
- context 'setting could not be updated' do
- let(:service) { instance_double('Projects::UpdateService') }
-
- before do
- allow(Projects::UpdateService).to receive(:new).and_return(service)
- allow(service).to receive(:execute).and_return(status: :error, message: 'Some error has occured')
- end
-
- it 'tries to change the setting' do
- visit project_pages_path(project)
-
- uncheck :project_pages_https_only
-
- click_button 'Save'
-
- expect(page).to have_text('Some error has occured')
- end
- end
-
- context 'non-HTTPS domain exists' do
- let(:project) { create(:project, pages_https_only: false) }
-
- before do
- create(:pages_domain, :without_key, :without_certificate, project: project)
- end
-
- it 'the setting is disabled' do
- visit project_pages_path(project)
-
- expect(page).to have_field(:project_pages_https_only, disabled: true)
- expect(page).to have_button('Save')
- end
- end
-
- context 'HTTPS pages are disabled', :https_pages_disabled do
- it 'the setting is unavailable' do
- visit project_pages_path(project)
-
- expect(page).not_to have_field(:project_pages_https_only)
- expect(page).not_to have_content('Force HTTPS (requires valid certificates)')
- expect(page).to have_button('Save')
- end
- end
- end
-
- describe 'Remove page' do
- let(:project) { create :project, :repository }
-
- context 'when pages are deployed' do
- let(:pipeline) do
- commit_sha = project.commit('HEAD').sha
-
- project.ci_pipelines.create(
- ref: 'HEAD',
- sha: commit_sha,
- source: :push,
- protected: false
- )
- end
-
- let(:ci_build) do
- create(
- :ci_build,
- project: project,
- pipeline: pipeline,
- ref: 'HEAD')
- end
-
- let!(:artifact) do
- create(:ci_job_artifact, :archive, :correct_checksum,
- file: fixture_file_upload(File.join('spec/fixtures/pages.zip')), job: ci_build)
- end
-
- let!(:metadata) do
- create(:ci_job_artifact, :metadata,
- file: fixture_file_upload(File.join('spec/fixtures/pages.zip.meta')), job: ci_build)
- end
-
- before do
- result = Projects::UpdatePagesService.new(project, ci_build).execute
- expect(result[:status]).to eq(:success)
- expect(project).to be_pages_deployed
- end
-
- it 'removes the pages', :sidekiq_inline do
- visit project_pages_path(project)
-
- expect(page).to have_link('Remove pages')
-
- accept_confirm { click_link 'Remove pages' }
-
- expect(page).to have_content('Pages were scheduled for removal')
- expect(project.reload.pages_deployed?).to be_falsey
- end
- end
- end
-end
-
-RSpec.describe 'Pages', :js do
- include LetsEncryptHelpers
-
- context 'when editing normally' do
- include_examples 'pages settings editing'
- end
-
- context 'when letsencrypt support is enabled' do
- before do
- stub_lets_encrypt_settings
- end
-
- include_examples 'pages settings editing'
- end
-end
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index ac3566fbbdd..94800717677 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -14,7 +14,6 @@ RSpec.describe 'Pipeline', :js do
before do
sign_in(user)
project.add_role(user, role)
- stub_feature_flags(graphql_pipeline_details: false)
end
shared_context 'pipeline builds' do
@@ -57,7 +56,7 @@ RSpec.describe 'Pipeline', :js do
end
end
- describe 'GET /:project/pipelines/:id' do
+ describe 'GET /:project/-/pipelines/:id' do
include_context 'pipeline builds'
let(:group) { create(:group) }
@@ -69,7 +68,7 @@ RSpec.describe 'Pipeline', :js do
it 'shows the pipeline graph' do
visit_pipeline
- expect(page).to have_selector('.pipeline-visualization')
+ expect(page).to have_selector('.js-pipeline-graph')
expect(page).to have_content('Build')
expect(page).to have_content('Test')
expect(page).to have_content('Deploy')
@@ -625,20 +624,6 @@ RSpec.describe 'Pipeline', :js do
end
end
end
-
- context 'when FF dag_pipeline_tab is disabled' do
- before do
- stub_feature_flags(dag_pipeline_tab: false)
- visit_pipeline
- end
-
- it 'does not show DAG link' do
- expect(page).to have_link('Pipeline')
- expect(page).to have_link('Jobs')
- expect(page).not_to have_link('DAG')
- expect(page).to have_link('Failed Jobs')
- end
- end
end
context 'when user does not have access to read jobs' do
@@ -646,7 +631,7 @@ RSpec.describe 'Pipeline', :js do
project.update(public_builds: false)
end
- describe 'GET /:project/pipelines/:id' do
+ describe 'GET /:project/-/pipelines/:id' do
include_context 'pipeline builds'
let(:project) { create(:project, :repository) }
@@ -657,7 +642,7 @@ RSpec.describe 'Pipeline', :js do
end
it 'shows the pipeline graph' do
- expect(page).to have_selector('.pipeline-visualization')
+ expect(page).to have_selector('.js-pipeline-graph')
expect(page).to have_content('Build')
expect(page).to have_content('Test')
expect(page).to have_content('Deploy')
@@ -691,13 +676,13 @@ RSpec.describe 'Pipeline', :js do
downstream: downstream)
end
- describe 'GET /:project/pipelines/:id' do
+ describe 'GET /:project/-/pipelines/:id' do
before do
visit project_pipeline_path(project, pipeline)
end
it 'shows the pipeline with a bridge job' do
- expect(page).to have_selector('.pipeline-visualization')
+ expect(page).to have_selector('.js-pipeline-graph')
expect(page).to have_content('cross-build')
end
@@ -740,7 +725,7 @@ RSpec.describe 'Pipeline', :js do
end
end
- describe 'GET /:project/pipelines/:id/builds' do
+ describe 'GET /:project/-/pipelines/:id/builds' do
before do
visit builds_project_pipeline_path(project, pipeline)
end
@@ -767,9 +752,64 @@ RSpec.describe 'Pipeline', :js do
stage_idx: 2, pipeline: pipeline, project: project, resource_group: resource_group)
end
- describe 'GET /:project/pipelines/:id' do
+ describe 'GET /:project/-/pipelines/:id' do
subject { visit project_pipeline_path(project, pipeline) }
+ # remove when :graphql_pipeline_details flag is removed
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/299112
+ context 'when :graphql_pipeline_details flag is off' do
+ before do
+ stub_feature_flags(graphql_pipeline_details: false)
+ stub_feature_flags(graphql_pipeline_details_users: false)
+ end
+
+ it 'shows deploy job as created' do
+ subject
+
+ within('.pipeline-header-container') do
+ expect(page).to have_content('pending')
+ end
+
+ within('.js-pipeline-graph') do
+ within '.stage-column:nth-child(1)' do
+ expect(page).to have_content('test')
+ expect(page).to have_css('.ci-status-icon-pending')
+ end
+
+ within '.stage-column:nth-child(2)' do
+ expect(page).to have_content('deploy')
+ expect(page).to have_css('.ci-status-icon-created')
+ end
+ end
+ end
+
+ context 'when test job succeeded' do
+ before do
+ test_job.success!
+ end
+
+ it 'shows deploy job as pending' do
+ subject
+
+ within('.pipeline-header-container') do
+ expect(page).to have_content('running')
+ end
+
+ within('.pipeline-graph') do
+ within '.stage-column:nth-child(1)' do
+ expect(page).to have_content('test')
+ expect(page).to have_css('.ci-status-icon-success')
+ end
+
+ within '.stage-column:nth-child(2)' do
+ expect(page).to have_content('deploy')
+ expect(page).to have_css('.ci-status-icon-pending')
+ end
+ end
+ end
+ end
+ end
+
it 'shows deploy job as created' do
subject
@@ -777,13 +817,13 @@ RSpec.describe 'Pipeline', :js do
expect(page).to have_content('pending')
end
- within('.pipeline-graph') do
- within '.stage-column:nth-child(1)' do
+ within('.js-pipeline-graph') do
+ within(all('[data-testid="stage-column"]')[0]) do
expect(page).to have_content('test')
expect(page).to have_css('.ci-status-icon-pending')
end
- within '.stage-column:nth-child(2)' do
+ within(all('[data-testid="stage-column"]')[1]) do
expect(page).to have_content('deploy')
expect(page).to have_css('.ci-status-icon-created')
end
@@ -802,13 +842,13 @@ RSpec.describe 'Pipeline', :js do
expect(page).to have_content('running')
end
- within('.pipeline-graph') do
- within '.stage-column:nth-child(1)' do
+ within('.js-pipeline-graph') do
+ within(all('[data-testid="stage-column"]')[0]) do
expect(page).to have_content('test')
expect(page).to have_css('.ci-status-icon-success')
end
- within '.stage-column:nth-child(2)' do
+ within(all('[data-testid="stage-column"]')[1]) do
expect(page).to have_content('deploy')
expect(page).to have_css('.ci-status-icon-pending')
end
@@ -831,14 +871,37 @@ RSpec.describe 'Pipeline', :js do
expect(page).to have_content('waiting')
end
- within('.pipeline-graph') do
- within '.stage-column:nth-child(2)' do
+ within('.js-pipeline-graph') do
+ within(all('[data-testid="stage-column"]')[1]) do
expect(page).to have_content('deploy')
expect(page).to have_css('.ci-status-icon-waiting-for-resource')
end
end
end
+ # remove when :graphql_pipeline_details flag is removed
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/299112
+ context 'when :graphql_pipeline_details flag is off' do
+ before do
+ stub_feature_flags(graphql_pipeline_details: false)
+ stub_feature_flags(graphql_pipeline_details_users: false)
+ end
+ it 'shows deploy job as waiting for resource' do
+ subject
+
+ within('.pipeline-header-container') do
+ expect(page).to have_content('waiting')
+ end
+
+ within('.pipeline-graph') do
+ within '.stage-column:nth-child(2)' do
+ expect(page).to have_content('deploy')
+ expect(page).to have_css('.ci-status-icon-waiting-for-resource')
+ end
+ end
+ end
+ end
+
context 'when resource is released from another job' do
before do
another_job.success!
@@ -851,19 +914,86 @@ RSpec.describe 'Pipeline', :js do
expect(page).to have_content('running')
end
- within('.pipeline-graph') do
- within '.stage-column:nth-child(2)' do
+ within('.js-pipeline-graph') do
+ within(all('[data-testid="stage-column"]')[1]) do
expect(page).to have_content('deploy')
expect(page).to have_css('.ci-status-icon-pending')
end
end
end
+
+ # remove when :graphql_pipeline_details flag is removed
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/299112
+ context 'when :graphql_pipeline_details flag is off' do
+ before do
+ stub_feature_flags(graphql_pipeline_details: false)
+ stub_feature_flags(graphql_pipeline_details_users: false)
+ end
+ it 'shows deploy job as pending' do
+ subject
+
+ within('.pipeline-header-container') do
+ expect(page).to have_content('running')
+ end
+
+ within('.pipeline-graph') do
+ within '.stage-column:nth-child(2)' do
+ expect(page).to have_content('deploy')
+ expect(page).to have_css('.ci-status-icon-pending')
+ end
+ end
+ end
+ end
+ end
+
+ context 'when deploy job is a bridge to trigger a downstream pipeline' do
+ let!(:deploy_job) do
+ create(:ci_bridge, :created, stage: 'deploy', name: 'deploy',
+ stage_idx: 2, pipeline: pipeline, project: project, resource_group: resource_group)
+ end
+
+ it 'shows deploy job as waiting for resource' do
+ subject
+
+ within('.pipeline-header-container') do
+ expect(page).to have_content('waiting')
+ end
+
+ within('.js-pipeline-graph') do
+ within(all('[data-testid="stage-column"]')[1]) do
+ expect(page).to have_content('deploy')
+ expect(page).to have_css('.ci-status-icon-waiting-for-resource')
+ end
+ end
+ end
+ end
+
+ context 'when deploy job is a bridge to trigger a downstream pipeline' do
+ let!(:deploy_job) do
+ create(:ci_bridge, :created, stage: 'deploy', name: 'deploy',
+ stage_idx: 2, pipeline: pipeline, project: project, resource_group: resource_group)
+ end
+
+ it 'shows deploy job as waiting for resource' do
+ subject
+
+ within('.pipeline-header-container') do
+ expect(page).to have_content('waiting')
+ end
+
+ within('.js-pipeline-graph') do
+ within(all('[data-testid="stage-column"]')[1]) do
+ expect(page).to have_content('deploy')
+ expect(page).to have_css('.ci-status-icon-waiting-for-resource')
+ end
+ end
+ end
end
end
end
end
- describe 'GET /:project/pipelines/:id/builds' do
+ describe 'GET /:project/-/pipelines/:id/builds' do
include_context 'pipeline builds'
let(:project) { create(:project, :repository) }
@@ -965,7 +1095,7 @@ RSpec.describe 'Pipeline', :js do
end
end
- describe 'GET /:project/pipelines/:id/failures' do
+ describe 'GET /:project/-/pipelines/:id/failures' do
let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: '1234') }
let(:pipeline_failures_page) { failures_project_pipeline_path(project, pipeline) }
let!(:failed_build) { create(:ci_build, :failed, pipeline: pipeline) }
@@ -1078,12 +1208,29 @@ RSpec.describe 'Pipeline', :js do
expect(current_path).to eq(pipeline_path(pipeline))
expect(page).not_to have_content('Failed Jobs')
- expect(page).to have_selector('.pipeline-visualization')
+ expect(page).to have_selector('.js-pipeline-graph')
+ end
+
+ # remove when :graphql_pipeline_details flag is removed
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/299112
+ context 'when :graphql_pipeline_details flag is off' do
+ before do
+ stub_feature_flags(graphql_pipeline_details: false)
+ stub_feature_flags(graphql_pipeline_details_users: false)
+ end
+
+ it 'displays the pipeline graph' do
+ subject
+
+ expect(current_path).to eq(pipeline_path(pipeline))
+ expect(page).not_to have_content('Failed Jobs')
+ expect(page).to have_selector('.pipeline-visualization')
+ end
end
end
end
- describe 'GET /:project/pipelines/:id/dag' do
+ describe 'GET /:project/-/pipelines/:id/dag' do
include_context 'pipeline builds'
let(:project) { create(:project, :repository) }
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index 450524b8d70..6421d3db2cd 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -13,11 +13,13 @@ RSpec.describe 'Pipelines', :js do
before do
sign_in(user)
stub_feature_flags(graphql_pipeline_details: false)
+ stub_feature_flags(graphql_pipeline_details_users: false)
+
project.add_developer(user)
project.update!(auto_devops_attributes: { enabled: false })
end
- describe 'GET /:project/pipelines' do
+ describe 'GET /:project/-/pipelines' do
let(:project) { create(:project, :repository) }
let!(:pipeline) do
@@ -287,23 +289,23 @@ RSpec.describe 'Pipelines', :js do
end
it 'has a dropdown with play button' do
- expect(page).to have_selector('.dropdown-new.btn.btn-default .icon-play')
+ expect(page).to have_selector('[data-testid="pipelines-manual-actions-dropdown"] [data-testid="play-icon"]')
end
it 'has link to the manual action' do
- find('.js-pipeline-dropdown-manual-actions').click
+ find('[data-testid="pipelines-manual-actions-dropdown"]').click
expect(page).to have_button('manual build')
end
context 'when manual action was played' do
before do
- find('.js-pipeline-dropdown-manual-actions').click
+ find('[data-testid="pipelines-manual-actions-dropdown"]').click
click_button('manual build')
end
it 'enqueues manual action job' do
- expect(page).to have_selector('.js-pipeline-dropdown-manual-actions:disabled')
+ expect(page).to have_selector('[data-testid="pipelines-manual-actions-dropdown"] .gl-dropdown-toggle:disabled')
end
end
end
@@ -321,11 +323,11 @@ RSpec.describe 'Pipelines', :js do
end
it 'has a dropdown for actionable jobs' do
- expect(page).to have_selector('.dropdown-new.btn.btn-default .icon-play')
+ expect(page).to have_selector('[data-testid="pipelines-manual-actions-dropdown"] [data-testid="play-icon"]')
end
it "has link to the delayed job's action" do
- find('.js-pipeline-dropdown-manual-actions').click
+ find('[data-testid="pipelines-manual-actions-dropdown"]').click
time_diff = [0, delayed_job.scheduled_at - Time.now].max
expect(page).to have_button('delayed job 1')
@@ -341,7 +343,7 @@ RSpec.describe 'Pipelines', :js do
end
it "shows 00:00:00 as the remaining time" do
- find('.js-pipeline-dropdown-manual-actions').click
+ find('[data-testid="pipelines-manual-actions-dropdown"]').click
expect(page).to have_content("00:00:00")
end
@@ -349,7 +351,7 @@ RSpec.describe 'Pipelines', :js do
context 'when user played a delayed job immediately' do
before do
- find('.js-pipeline-dropdown-manual-actions').click
+ find('[data-testid="pipelines-manual-actions-dropdown"]').click
page.accept_confirm { click_button('delayed job 1') }
wait_for_requests
end
@@ -517,56 +519,75 @@ RSpec.describe 'Pipelines', :js do
end
end
- context 'mini pipeline graph' do
- let!(:build) do
- create(:ci_build, :pending, pipeline: pipeline,
- stage: 'build',
- name: 'build')
- end
-
- before do
- visit_project_pipelines
- end
+ shared_examples 'mini pipeline renders' do |ci_mini_pipeline_gl_dropdown_enabled|
+ context 'mini pipeline graph' do
+ let!(:build) do
+ create(:ci_build, :pending, pipeline: pipeline,
+ stage: 'build',
+ name: 'build')
+ end
- it 'renders a mini pipeline graph' do
- expect(page).to have_selector('[data-testid="widget-mini-pipeline-graph"]')
- expect(page).to have_selector('.js-builds-dropdown-button')
- end
+ before do
+ stub_feature_flags(ci_mini_pipeline_gl_dropdown: ci_mini_pipeline_gl_dropdown_enabled)
+ visit_project_pipelines
+ end
- context 'when clicking a stage badge' do
- it 'opens a dropdown' do
- find('.js-builds-dropdown-button').click
+ let_it_be(:dropdown_toggle_selector) do
+ if ci_mini_pipeline_gl_dropdown_enabled
+ '[data-testid="mini-pipeline-graph-dropdown"] .dropdown-toggle'
+ else
+ '[data-testid="mini-pipeline-graph-dropdown-toggle"]'
+ end
+ end
- expect(page).to have_link build.name
+ it 'renders a mini pipeline graph' do
+ expect(page).to have_selector('[data-testid="widget-mini-pipeline-graph"]')
+ expect(page).to have_selector(dropdown_toggle_selector)
end
- it 'is possible to cancel pending build' do
- find('.js-builds-dropdown-button').click
- find('.js-ci-action').click
- wait_for_requests
+ context 'when clicking a stage badge' do
+ it 'opens a dropdown' do
+ find(dropdown_toggle_selector).click
- expect(build.reload).to be_canceled
- end
- end
+ expect(page).to have_link build.name
+ end
- context 'for a failed pipeline' do
- let!(:build) do
- create(:ci_build, :failed, pipeline: pipeline,
- stage: 'build',
- name: 'build')
+ it 'is possible to cancel pending build' do
+ find(dropdown_toggle_selector).click
+ find('.js-ci-action').click
+ wait_for_requests
+
+ expect(build.reload).to be_canceled
+ end
end
- it 'displays the failure reason' do
- find('.js-builds-dropdown-button').click
+ context 'for a failed pipeline' do
+ let!(:build) do
+ create(:ci_build, :failed, pipeline: pipeline,
+ stage: 'build',
+ name: 'build')
+ end
+
+ it 'displays the failure reason' do
+ find(dropdown_toggle_selector).click
- within('.js-builds-dropdown-list') do
- build_element = page.find('.mini-pipeline-graph-dropdown-item')
- expect(build_element['title']).to eq('build - failed - (unknown failure)')
+ within('.js-builds-dropdown-list') do
+ build_element = page.find('.mini-pipeline-graph-dropdown-item')
+ expect(build_element['title']).to eq('build - failed - (unknown failure)')
+ end
end
end
end
end
+ context 'with ci_mini_pipeline_gl_dropdown disabled' do
+ it_behaves_like "mini pipeline renders", false
+ end
+
+ context 'with ci_mini_pipeline_gl_dropdown enabled' do
+ it_behaves_like "mini pipeline renders", true
+ end
+
context 'with pagination' do
before do
allow(Ci::Pipeline).to receive(:default_per_page).and_return(1)
@@ -597,7 +618,7 @@ RSpec.describe 'Pipelines', :js do
end
end
- describe 'GET /:project/pipelines/show' do
+ describe 'GET /:project/-/pipelines/show' do
let(:project) { create(:project, :repository) }
let(:pipeline) do
@@ -649,7 +670,7 @@ RSpec.describe 'Pipelines', :js do
end
end
- describe 'POST /:project/pipelines' do
+ describe 'POST /:project/-/pipelines' do
let(:project) { create(:project, :repository) }
before do
diff --git a/spec/features/projects/services/user_activates_slack_slash_command_spec.rb b/spec/features/projects/services/user_activates_slack_slash_command_spec.rb
index 3994f55caee..4dfd4416eeb 100644
--- a/spec/features/projects/services/user_activates_slack_slash_command_spec.rb
+++ b/spec/features/projects/services/user_activates_slack_slash_command_spec.rb
@@ -40,4 +40,8 @@ RSpec.describe 'Slack slash commands', :js do
value = find_field('url').value
expect(value).to match("api/v4/projects/#{project.id}/services/slack_slash_commands/trigger")
end
+
+ it 'shows help content' do
+ expect(page).to have_content('This service allows users to perform common operations on this project by entering slash commands in Slack.')
+ end
end
diff --git a/spec/features/projects/settings/pipelines_settings_spec.rb b/spec/features/projects/settings/pipelines_settings_spec.rb
index c087237fd7c..39c4315bf0f 100644
--- a/spec/features/projects/settings/pipelines_settings_spec.rb
+++ b/spec/features/projects/settings/pipelines_settings_spec.rb
@@ -46,7 +46,7 @@ RSpec.describe "Projects > Settings > Pipelines settings" do
it 'updates auto_cancel_pending_pipelines' do
visit project_settings_ci_cd_path(project)
- page.check('Auto-cancel redundant, pending pipelines')
+ page.check('Auto-cancel redundant pipelines')
page.within '#js-general-pipeline-settings' do
click_on 'Save changes'
end
diff --git a/spec/features/projects/settings/project_settings_spec.rb b/spec/features/projects/settings/project_settings_spec.rb
index 7b2b5594c22..cd1c9ecde9c 100644
--- a/spec/features/projects/settings/project_settings_spec.rb
+++ b/spec/features/projects/settings/project_settings_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'Projects settings' do
let_it_be(:project) { create(:project) }
let(:user) { project.owner }
let(:panel) { find('.general-settings', match: :first) }
- let(:button) { panel.find('.btn.js-settings-toggle') }
+ let(:button) { panel.find('.btn.gl-button.js-settings-toggle') }
let(:title) { panel.find('.settings-title') }
before do
@@ -39,7 +39,7 @@ RSpec.describe 'Projects settings' do
visit edit_project_path(project)
forking_enabled_input = find('input[name="project[project_feature_attributes][forking_access_level]"]', visible: :hidden)
- forking_enabled_button = find('input[name="project[project_feature_attributes][forking_access_level]"] + label > button')
+ forking_enabled_button = find('[data-for="project[project_feature_attributes][forking_access_level]"] .gl-toggle')
expect(forking_enabled_input.value).to eq('20')
diff --git a/spec/features/projects/settings/registry_settings_spec.rb b/spec/features/projects/settings/registry_settings_spec.rb
index 2b03ecf5af1..6e4082d1391 100644
--- a/spec/features/projects/settings/registry_settings_spec.rb
+++ b/spec/features/projects/settings/registry_settings_spec.rb
@@ -39,7 +39,7 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
select('7 days', from: 'Remove tags older than:')
fill_in('Remove tags matching:', with: '.*-production')
- submit_button = find('.btn.btn-success')
+ submit_button = find('.btn.gl-button.btn-success')
expect(submit_button).not_to be_disabled
submit_button.click
end
@@ -53,7 +53,7 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
within '#js-registry-policies' do
fill_in('Remove tags matching:', with: '*-production')
- submit_button = find('.btn.btn-success')
+ submit_button = find('.btn.gl-button.btn-success')
expect(submit_button).not_to be_disabled
submit_button.click
end
diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb
index 3e520142117..2f257d299d8 100644
--- a/spec/features/projects/settings/repository_settings_spec.rb
+++ b/spec/features/projects/settings/repository_settings_spec.rb
@@ -63,7 +63,7 @@ RSpec.describe 'Projects > Settings > Repository settings' do
click_button 'Add key'
expect(page).to have_content('new_deploy_key')
- expect(page).to have_content('Write access allowed')
+ expect(page).to have_content('Grant write permissions to this key')
end
it 'edit an existing deploy key' do
@@ -77,7 +77,7 @@ RSpec.describe 'Projects > Settings > Repository settings' do
click_button 'Save changes'
expect(page).to have_content('updated_deploy_key')
- expect(page).to have_content('Write access allowed')
+ expect(page).to have_content('Grant write permissions to this key')
end
it 'edit an existing public deploy key to be writable' do
@@ -90,7 +90,7 @@ RSpec.describe 'Projects > Settings > Repository settings' do
click_button 'Save changes'
expect(page).to have_content('public_deploy_key')
- expect(page).to have_content('Write access allowed')
+ expect(page).to have_content('Grant write permissions to this key')
end
it 'edit a deploy key from projects user has access to' do
diff --git a/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb b/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb
index e97e4a2030a..e8e32d93f7b 100644
--- a/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb
+++ b/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb
@@ -51,7 +51,7 @@ RSpec.describe 'Projects > Settings > User manages merge request settings' do
expect(page).to have_content 'All discussions must be resolved'
within('.sharing-permissions-form') do
- find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .project-feature-toggle').click
+ find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .gl-toggle').click
find('input[value="Save changes"]').send_keys(:return)
end
@@ -71,7 +71,7 @@ RSpec.describe 'Projects > Settings > User manages merge request settings' do
expect(page).to have_content 'All discussions must be resolved'
within('.sharing-permissions-form') do
- find('.project-feature-controls[data-for="project[project_feature_attributes][builds_access_level]"] .project-feature-toggle').click
+ find('.project-feature-controls[data-for="project[project_feature_attributes][builds_access_level]"] .gl-toggle').click
find('input[value="Save changes"]').send_keys(:return)
end
@@ -92,7 +92,7 @@ RSpec.describe 'Projects > Settings > User manages merge request settings' do
expect(page).not_to have_content 'All discussions must be resolved'
within('.sharing-permissions-form') do
- find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .project-feature-toggle').click
+ find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .gl-toggle').click
find('input[value="Save changes"]').send_keys(:return)
end
diff --git a/spec/features/projects/settings/user_manages_project_members_spec.rb b/spec/features/projects/settings/user_manages_project_members_spec.rb
index 726b8fb6840..0d22da34b91 100644
--- a/spec/features/projects/settings/user_manages_project_members_spec.rb
+++ b/spec/features/projects/settings/user_manages_project_members_spec.rb
@@ -3,6 +3,9 @@
require 'spec_helper'
RSpec.describe 'Projects > Settings > User manages project members' do
+ include Spec::Support::Helpers::Features::MembersHelpers
+ include Select2Helper
+
let(:group) { create(:group, name: 'OpenSource') }
let(:project) { create(:project) }
let(:project2) { create(:project) }
@@ -16,62 +19,123 @@ RSpec.describe 'Projects > Settings > User manages project members' do
sign_in(user)
end
- it 'cancels a team member', :js do
- visit(project_project_members_path(project))
+ context 'when `vue_project_members_list` feature flag is enabled' do
+ it 'cancels a team member', :js do
+ visit(project_project_members_path(project))
+
+ page.within find_member_row(user_dmitriy) do
+ click_button 'Remove member'
+ end
+
+ page.within('[role="dialog"]') do
+ expect(page).to have_unchecked_field 'Also unassign this user from related issues and merge requests'
+ click_button('Remove member')
+ end
+
+ visit(project_project_members_path(project))
+
+ expect(members_table).not_to have_content(user_dmitriy.name)
+ expect(members_table).not_to have_content(user_dmitriy.username)
+ end
+
+ it 'imports a team from another project', :js do
+ stub_feature_flags(invite_members_group_modal: false)
+
+ project2.add_maintainer(user)
+ project2.add_reporter(user_mike)
+
+ visit(project_project_members_path(project))
- project_member = project.project_members.find_by(user_id: user_dmitriy.id)
+ page.within('.invite-users-form') do
+ click_link('Import')
+ end
- page.within("#project_member_#{project_member.id}") do
- # Open modal
- click_on('Remove user from project')
+ select2(project2.id, from: '#source_project_id')
+ click_button('Import project members')
+
+ expect(find_member_row(user_mike)).to have_content('Reporter')
end
- expect(page).to have_unchecked_field 'Also unassign this user from related issues and merge requests'
+ it 'shows all members of project shared group', :js do
+ group.add_owner(user)
+ group.add_developer(user_dmitriy)
+
+ share_link = project.project_group_links.new(group_access: Gitlab::Access::MAINTAINER)
+ share_link.group_id = group.id
+ share_link.save!
- click_on('Remove member')
+ visit(project_project_members_path(project))
- visit(project_project_members_path(project))
+ click_link 'Groups'
- expect(page).not_to have_content(user_dmitriy.name)
- expect(page).not_to have_content(user_dmitriy.username)
+ expect(find_group_row(group)).to have_content('Maintainer')
+ end
end
- it 'imports a team from another project' do
- project2.add_maintainer(user)
- project2.add_reporter(user_mike)
+ context 'when `vue_project_members_list` feature flag is disabled' do
+ before do
+ stub_feature_flags(vue_project_members_list: false)
+ end
+
+ it 'cancels a team member', :js do
+ visit(project_project_members_path(project))
+
+ project_member = project.project_members.find_by(user_id: user_dmitriy.id)
+
+ page.within("#project_member_#{project_member.id}") do
+ # Open modal
+ click_on('Remove user from project')
+ end
+
+ expect(page).to have_unchecked_field 'Also unassign this user from related issues and merge requests'
+
+ click_on('Remove member')
- visit(project_project_members_path(project))
+ visit(project_project_members_path(project))
- page.within('.invite-users-form') do
- click_link('Import')
+ expect(page).not_to have_content(user_dmitriy.name)
+ expect(page).not_to have_content(user_dmitriy.username)
end
- select(project2.full_name, from: 'source_project_id')
- click_button('Import')
+ it 'imports a team from another project' do
+ stub_feature_flags(invite_members_group_modal: false)
- project_member = project.project_members.find_by(user_id: user_mike.id)
+ project2.add_maintainer(user)
+ project2.add_reporter(user_mike)
- page.within("#project_member_#{project_member.id}") do
- expect(page).to have_content('Mike')
- expect(page).to have_content('Reporter')
+ visit(project_project_members_path(project))
+
+ page.within('.invite-users-form') do
+ click_link('Import')
+ end
+
+ select(project2.full_name, from: 'source_project_id')
+ click_button('Import')
+
+ project_member = project.project_members.find_by(user_id: user_mike.id)
+
+ page.within("#project_member_#{project_member.id}") do
+ expect(page).to have_content('Mike')
+ expect(page).to have_content('Reporter')
+ end
end
- end
- it 'shows all members of project shared group', :js do
- group.add_owner(user)
- group.add_developer(user_dmitriy)
+ it 'shows all members of project shared group', :js do
+ group.add_owner(user)
+ group.add_developer(user_dmitriy)
- share_link = project.project_group_links.new(group_access: Gitlab::Access::MAINTAINER)
- share_link.group_id = group.id
- share_link.save!
+ share_link = project.project_group_links.new(group_access: Gitlab::Access::MAINTAINER)
+ share_link.group_id = group.id
+ share_link.save!
- visit(project_project_members_path(project))
+ visit(project_project_members_path(project))
- click_link 'Groups'
+ click_link 'Groups'
- page.within('[data-testid="project-member-groups"]') do
- expect(page).to have_content('OpenSource')
- expect(first('.group_member')).to have_content('Maintainer')
+ page.within('[data-testid="project-member-groups"]') do
+ expect(page).to have_content('OpenSource')
+ expect(first('.group_member')).to have_content('Maintainer')
+ end
end
end
end
diff --git a/spec/features/projects/settings/visibility_settings_spec.rb b/spec/features/projects/settings/visibility_settings_spec.rb
index 6cecbbdb3d0..becb30c02b7 100644
--- a/spec/features/projects/settings/visibility_settings_spec.rb
+++ b/spec/features/projects/settings/visibility_settings_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe 'Projects > Settings > Visibility settings', :js do
context 'merge requests select' do
it 'hides merge requests section' do
- find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .project-feature-toggle').click
+ find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .gl-toggle').click
expect(page).to have_selector('.merge-requests-feature', visible: false)
end
@@ -46,7 +46,7 @@ RSpec.describe 'Projects > Settings > Visibility settings', :js do
context 'builds select' do
it 'hides builds select section' do
- find('.project-feature-controls[data-for="project[project_feature_attributes][builds_access_level]"] .project-feature-toggle').click
+ find('.project-feature-controls[data-for="project[project_feature_attributes][builds_access_level]"] .gl-toggle').click
expect(page).to have_selector('.builds-feature', visible: false)
end
diff --git a/spec/features/projects/show/user_manages_notifications_spec.rb b/spec/features/projects/show/user_manages_notifications_spec.rb
index d444ea27d35..5f7d9b0963b 100644
--- a/spec/features/projects/show/user_manages_notifications_spec.rb
+++ b/spec/features/projects/show/user_manages_notifications_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe 'Projects > Show > User manages notifications', :js do
let(:project) { create(:project, :public, :repository) }
before do
+ stub_feature_flags(vue_notification_dropdown: false)
sign_in(project.owner)
end
diff --git a/spec/features/projects/show/user_sees_git_instructions_spec.rb b/spec/features/projects/show/user_sees_git_instructions_spec.rb
index febdb70de86..e6157887c12 100644
--- a/spec/features/projects/show/user_sees_git_instructions_spec.rb
+++ b/spec/features/projects/show/user_sees_git_instructions_spec.rb
@@ -5,6 +5,13 @@ require 'spec_helper'
RSpec.describe 'Projects > Show > User sees Git instructions' do
let_it_be(:user) { create(:user) }
+ before do
+ # Reset user notification settings between examples to prevent
+ # validation failure on NotificationSetting.
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/299822#note_492817174
+ user.notification_settings.reset
+ end
+
shared_examples_for 'redirects to the sign in page' do
it 'redirects to the sign in page' do
expect(current_path).to eq(new_user_session_path)
diff --git a/spec/features/projects/terraform_spec.rb b/spec/features/projects/terraform_spec.rb
index dfa4dad8490..55b906c2bc5 100644
--- a/spec/features/projects/terraform_spec.rb
+++ b/spec/features/projects/terraform_spec.rb
@@ -68,7 +68,7 @@ RSpec.describe 'Terraform', :js do
fill_in "terraform-state-remove-input-#{additional_state.name}", with: additional_state.name
click_button 'Remove'
- expect(page).not_to have_content(additional_state.name)
+ expect(page).to have_content("#{additional_state.name} successfully removed")
expect { additional_state.reload }.to raise_error ActiveRecord::RecordNotFound
end
end
diff --git a/spec/features/projects/user_creates_project_spec.rb b/spec/features/projects/user_creates_project_spec.rb
index feb5f348256..aff3022bd4e 100644
--- a/spec/features/projects/user_creates_project_spec.rb
+++ b/spec/features/projects/user_creates_project_spec.rb
@@ -8,6 +8,8 @@ RSpec.describe 'User creates a project', :js do
before do
sign_in(user)
create(:personal_key, user: user)
+
+ stub_experiments(new_project_readme: :candidate)
end
it 'creates a new project' do
@@ -16,6 +18,10 @@ RSpec.describe 'User creates a project', :js do
find('[data-qa-selector="blank_project_link"]').click
fill_in(:project_name, with: 'Empty')
+ # part of the new_project_readme experiment
+ expect(page).to have_checked_field 'Initialize repository with a README'
+ uncheck 'Initialize repository with a README'
+
page.within('#content-body') do
click_button('Create project')
end
diff --git a/spec/features/projects/user_sees_user_popover_spec.rb b/spec/features/projects/user_sees_user_popover_spec.rb
index 9cfc6234969..52e65deae3b 100644
--- a/spec/features/projects/user_sees_user_popover_spec.rb
+++ b/spec/features/projects/user_sees_user_popover_spec.rb
@@ -38,8 +38,6 @@ RSpec.describe 'User sees user popover', :js do
it "displays user popover in system note" do
add_note("/assign @#{user.username}")
- wait_for_requests
-
find('.system-note-message .js-user-link').hover
page.within(popover_selector) do
diff --git a/spec/features/projects/user_uses_shortcuts_spec.rb b/spec/features/projects/user_uses_shortcuts_spec.rb
index 8fa5f741a95..13ae035e8ef 100644
--- a/spec/features/projects/user_uses_shortcuts_spec.rb
+++ b/spec/features/projects/user_uses_shortcuts_spec.rb
@@ -27,14 +27,13 @@ RSpec.describe 'User uses shortcuts', :js do
open_modal_shortcut_keys
- # modal-shortcuts still in the DOM, but hidden
- expect(find('#modal-shortcuts', visible: false)).not_to be_visible
+ expect(page).not_to have_selector('[data-testid="modal-shortcuts"]')
page.refresh
open_modal_shortcut_keys
# after reload, shortcuts modal doesn't exist at all until we add it
- expect(page).not_to have_selector('#modal-shortcuts')
+ expect(page).not_to have_selector('[data-testid="modal-shortcuts"]')
end
it 're-enables shortcuts' do
@@ -47,7 +46,7 @@ RSpec.describe 'User uses shortcuts', :js do
close_modal
open_modal_shortcut_keys
- expect(find('#modal-shortcuts')).to be_visible
+ expect(find('[data-testid="modal-shortcuts"]')).to be_visible
end
def open_modal_shortcut_keys
diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb
index 95d268ab1be..eb099359df9 100644
--- a/spec/features/protected_branches_spec.rb
+++ b/spec/features/protected_branches_spec.rb
@@ -9,10 +9,6 @@ RSpec.describe 'Protected Branches', :js do
let(:admin) { create(:admin) }
let(:project) { create(:project, :repository) }
- before do
- stub_feature_flags(deploy_keys_on_protected_branches: false)
- end
-
context 'logged in as developer' do
before do
project.add_developer(user)
@@ -174,7 +170,7 @@ RSpec.describe 'Protected Branches', :js do
stub_licensed_features(protected_refs_for_users: false)
end
- include_examples 'when the deploy_keys_on_protected_branches FF is turned on' do
+ include_examples 'Deploy keys with protected branches' do
let(:all_dropdown_sections) { %w(Roles Deploy\ Keys) }
end
end
diff --git a/spec/features/registrations/experience_level_spec.rb b/spec/features/registrations/experience_level_spec.rb
index 25496e2fef1..f432215d4a8 100644
--- a/spec/features/registrations/experience_level_spec.rb
+++ b/spec/features/registrations/experience_level_spec.rb
@@ -9,7 +9,6 @@ RSpec.describe 'Experience level screen' do
before do
group.add_owner(user)
gitlab_sign_in(user)
- stub_experiment_for_subject(onboarding_issues: true)
visit users_sign_up_experience_level_path(namespace_path: group.to_param)
end
diff --git a/spec/features/search/user_searches_for_commits_spec.rb b/spec/features/search/user_searches_for_commits_spec.rb
index b860cd08e64..1a882050126 100644
--- a/spec/features/search/user_searches_for_commits_spec.rb
+++ b/spec/features/search/user_searches_for_commits_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User searches for commits' do
+RSpec.describe 'User searches for commits', :js do
let(:project) { create(:project, :repository) }
let(:sha) { '6d394385cf567f80a8fd85055db1ab4c5295806f' }
let(:user) { create(:user) }
@@ -41,7 +41,7 @@ RSpec.describe 'User searches for commits' do
submit_search('See merge request')
select_search_scope('Commits')
- expect(page).to have_selector('.commit-row-description', count: 9)
+ expect(page).to have_selector('.commit-row-description', visible: false, count: 9)
end
end
end
diff --git a/spec/features/search/user_searches_for_issues_spec.rb b/spec/features/search/user_searches_for_issues_spec.rb
index e253b9f2f7a..828e478d701 100644
--- a/spec/features/search/user_searches_for_issues_spec.rb
+++ b/spec/features/search/user_searches_for_issues_spec.rb
@@ -75,7 +75,7 @@ RSpec.describe 'User searches for issues', :js do
expect(page.all('.search-result-row').last).to have_link(issue1.title)
end
- find('.reverse-sort-btn').click
+ find('[data-testid="sort-highest-icon"]').click
page.within('.results') do
expect(page.all('.search-result-row').first).to have_link(issue1.title)
diff --git a/spec/features/search/user_searches_for_merge_requests_spec.rb b/spec/features/search/user_searches_for_merge_requests_spec.rb
index 21e8075739f..7271716644b 100644
--- a/spec/features/search/user_searches_for_merge_requests_spec.rb
+++ b/spec/features/search/user_searches_for_merge_requests_spec.rb
@@ -5,8 +5,14 @@ require 'spec_helper'
RSpec.describe 'User searches for merge requests', :js do
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
- let!(:merge_request1) { create(:merge_request, title: 'Foo', source_project: project, target_project: project) }
- let!(:merge_request2) { create(:merge_request, :simple, title: 'Bar', source_project: project, target_project: project) }
+ let!(:merge_request1) { create(:merge_request, title: 'Merge Request Foo', source_project: project, target_project: project, created_at: 1.hour.ago) }
+ let!(:merge_request2) { create(:merge_request, :simple, title: 'Merge Request Bar', source_project: project, target_project: project) }
+
+ def search_for_mr(search)
+ fill_in('dashboard_search', with: search)
+ find('.btn-search').click
+ select_search_scope('Merge requests')
+ end
before do
project.add_maintainer(user)
@@ -18,9 +24,7 @@ RSpec.describe 'User searches for merge requests', :js do
include_examples 'top right search form'
it 'finds a merge request' do
- fill_in('dashboard_search', with: merge_request1.title)
- find('.btn-search').click
- select_search_scope('Merge requests')
+ search_for_mr(merge_request1.title)
page.within('.results') do
expect(page).to have_link(merge_request1.title)
@@ -28,6 +32,22 @@ RSpec.describe 'User searches for merge requests', :js do
end
end
+ 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
+
+ find('[data-testid="sort-highest-icon"]').click
+
+ page.within('.results') do
+ expect(page.all('.search-result-row').first).to have_link(merge_request1.title)
+ expect(page.all('.search-result-row').last).to have_link(merge_request2.title)
+ end
+ end
+
context 'when on a project page' do
it 'finds a merge request' do
find('[data-testid="project-filter"]').click
@@ -38,9 +58,7 @@ RSpec.describe 'User searches for merge requests', :js do
click_on(project.full_name)
end
- fill_in('dashboard_search', with: merge_request1.title)
- find('.btn-search').click
- select_search_scope('Merge requests')
+ search_for_mr(merge_request1.title)
page.within('.results') do
expect(page).to have_link(merge_request1.title)
diff --git a/spec/features/search/user_searches_for_projects_spec.rb b/spec/features/search/user_searches_for_projects_spec.rb
index b64909dd42f..e34ae031679 100644
--- a/spec/features/search/user_searches_for_projects_spec.rb
+++ b/spec/features/search/user_searches_for_projects_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User searches for projects' do
+RSpec.describe 'User searches for projects', :js do
let!(:project) { create(:project, :public, name: 'Shop') }
context 'when signed out' do
diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb
index 0f8daaf8e15..e17521e1d02 100644
--- a/spec/features/task_lists_spec.rb
+++ b/spec/features/task_lists_spec.rb
@@ -69,7 +69,13 @@ RSpec.describe 'Task Lists', :js do
wait_for_requests
expect(page).to have_selector(".md .task-list .task-list-item .task-list-item-checkbox")
- expect(page).to have_selector('.btn-close')
+ end
+
+ it_behaves_like 'page with comment and close button', 'Close issue' do
+ def setup
+ visit_issue(project, issue)
+ wait_for_requests
+ end
end
it 'is only editable by author' do
diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb
index 5762a54a717..eed67e3ac78 100644
--- a/spec/features/u2f_spec.rb
+++ b/spec/features/u2f_spec.rb
@@ -39,6 +39,11 @@ RSpec.describe 'Using U2F (Universal 2nd Factor) Devices for Authentication', :j
end
it 'allows the same device to be registered for multiple users' do
+ # U2f specs will be removed after WebAuthn migration completed
+ pending('FakeU2fDevice has static key handle, '\
+ 'leading to duplicate credential_xid for WebAuthn during migration, '\
+ 'resulting in unique constraint violation')
+
# First user
visit profile_account_path
manage_two_factor_authentication
@@ -148,6 +153,11 @@ RSpec.describe 'Using U2F (Universal 2nd Factor) Devices for Authentication', :j
describe "and also the current user" do
it "allows logging in with that particular device" do
+ # U2f specs will be removed after WebAuthn migration completed
+ pending('FakeU2fDevice has static key handle, '\
+ 'leading to duplicate credential_xid for WebAuthn during migration, '\
+ 'resulting in unique constraint violation')
+
# Register current user with the same U2F device
current_user = gitlab_sign_in(:user)
current_user.update_attribute(:otp_required_for_login, true)
diff --git a/spec/features/user_sees_revert_modal_spec.rb b/spec/features/user_sees_revert_modal_spec.rb
index 331f51dad95..5edf8358244 100644
--- a/spec/features/user_sees_revert_modal_spec.rb
+++ b/spec/features/user_sees_revert_modal_spec.rb
@@ -7,26 +7,33 @@ RSpec.describe 'Merge request > User sees revert modal', :js, :sidekiq_might_not
let(:user) { project.creator }
let(:merge_request) { create(:merge_request, source_project: project) }
+ shared_examples 'showing the revert modal' do
+ it 'shows the revert modal' do
+ click_button('Revert')
+
+ page.within('[data-testid="modal-commit"]') do
+ expect(page).to have_content 'Revert this merge request'
+ end
+ end
+ end
+
before do
sign_in(user)
visit(project_merge_request_path(project, merge_request))
click_button('Merge')
wait_for_requests
-
- visit(merge_request_path(merge_request))
- click_link('Revert')
end
- it 'shows the revert modal' do
- page.within('.modal-header') do
- expect(page).to have_content 'Revert this merge request'
- end
+ context 'without page reload after merge validates js correctly loaded' do
+ it_behaves_like 'showing the revert modal'
end
- it 'closes the revert modal with escape keypress' do
- find('#modal-revert-commit').send_keys(:escape)
+ context 'with page reload validates js correctly loaded' do
+ before do
+ visit(merge_request_path(merge_request))
+ end
- expect(page).not_to have_selector('#modal-revert-commit', visible: true)
+ it_behaves_like 'showing the revert modal'
end
end
diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb
index 5a537c1d4df..1d1120709b5 100644
--- a/spec/features/users/login_spec.rb
+++ b/spec/features/users/login_spec.rb
@@ -765,7 +765,7 @@ RSpec.describe 'Login' do
click_link 'Proceed'
expect(current_path).to eq(profile_account_path)
- expect(page).to have_content('Congratulations! You have enabled Two-factor Authentication!')
+ expect(page).to have_content('You have set up 2FA for your account! If you lose access to your 2FA device, you can use your recovery codes to access your account. Alternatively, if you upload an SSH key, you can use that key to generate additional recovery codes.')
end
end
diff --git a/spec/features/users/overview_spec.rb b/spec/features/users/overview_spec.rb
index 67216b04504..902079b7b93 100644
--- a/spec/features/users/overview_spec.rb
+++ b/spec/features/users/overview_spec.rb
@@ -151,6 +151,132 @@ RSpec.describe 'Overview tab on a user profile', :js do
end
end
+ describe 'followers section' do
+ describe 'user has no followers' do
+ before do
+ visit user.username
+ page.find('.js-followers-tab a').click
+ wait_for_requests
+ end
+
+ it 'shows an empty followers list with an info message' do
+ page.within('#followers') do
+ expect(page).to have_content('You do not have any followers')
+ expect(page).not_to have_selector('.gl-card.gl-mb-5')
+ expect(page).not_to have_selector('.gl-pagination')
+ end
+ end
+ end
+
+ describe 'user has less then 20 followers' do
+ let(:follower) { create(:user) }
+
+ before do
+ follower.follow(user)
+ visit user.username
+ page.find('.js-followers-tab a').click
+ wait_for_requests
+ end
+
+ it 'shows followers' do
+ page.within('#followers') do
+ expect(page).to have_content(follower.name)
+ expect(page).to have_selector('.gl-card.gl-mb-5')
+ expect(page).not_to have_selector('.gl-pagination')
+ end
+ end
+ end
+
+ describe 'user has more then 20 followers' do
+ let(:other_users) { create_list(:user, 21) }
+
+ before do
+ other_users.each do |follower|
+ follower.follow(user)
+ end
+
+ visit user.username
+ page.find('.js-followers-tab a').click
+ wait_for_requests
+ end
+ it 'shows paginated followers' do
+ page.within('#followers') do
+ other_users.each_with_index do |follower, i|
+ break if i == 20
+
+ expect(page).to have_content(follower.name)
+ end
+ expect(page).to have_selector('.gl-card.gl-mb-5')
+ expect(page).to have_selector('.gl-pagination')
+ expect(page).to have_selector('.gl-pagination .js-pagination-page', count: 2)
+ end
+ end
+ end
+ end
+
+ describe 'following section' do
+ describe 'user is not following others' do
+ before do
+ visit user.username
+ page.find('.js-following-tab a').click
+ wait_for_requests
+ end
+
+ it 'shows an empty following list with an info message' do
+ page.within('#following') do
+ expect(page).to have_content('You are not following other users')
+ expect(page).not_to have_selector('.gl-card.gl-mb-5')
+ expect(page).not_to have_selector('.gl-pagination')
+ end
+ end
+ end
+
+ describe 'user is following less then 20 people' do
+ let(:followee) { create(:user) }
+
+ before do
+ user.follow(followee)
+ visit user.username
+ page.find('.js-following-tab a').click
+ wait_for_requests
+ end
+
+ it 'shows following user' do
+ page.within('#following') do
+ expect(page).to have_content(followee.name)
+ expect(page).to have_selector('.gl-card.gl-mb-5')
+ expect(page).not_to have_selector('.gl-pagination')
+ end
+ end
+ end
+
+ describe 'user is following more then 20 people' do
+ let(:other_users) { create_list(:user, 21) }
+
+ before do
+ other_users.each do |followee|
+ user.follow(followee)
+ end
+
+ visit user.username
+ page.find('.js-following-tab a').click
+ wait_for_requests
+ end
+ it 'shows paginated following' do
+ page.within('#following') do
+ other_users.each_with_index do |followee, i|
+ break if i == 20
+
+ expect(page).to have_content(followee.name)
+ end
+ expect(page).to have_selector('.gl-card.gl-mb-5')
+ expect(page).to have_selector('.gl-pagination')
+ expect(page).to have_selector('.gl-pagination .js-pagination-page', count: 2)
+ end
+ end
+ end
+ end
+
describe 'bot user' do
let(:bot_user) { create(:user, user_type: :security_bot) }
diff --git a/spec/features/users/show_spec.rb b/spec/features/users/show_spec.rb
index 6aeb3023db8..a8372800700 100644
--- a/spec/features/users/show_spec.rb
+++ b/spec/features/users/show_spec.rb
@@ -20,6 +20,8 @@ RSpec.describe 'User page' do
expect(page).to have_link('Contributed projects')
expect(page).to have_link('Personal projects')
expect(page).to have_link('Snippets')
+ expect(page).to have_link('Followers')
+ expect(page).to have_link('Following')
end
end
@@ -54,6 +56,50 @@ RSpec.describe 'User page' do
expect(page).to have_content('GitLab - work info test')
end
end
+
+ context 'follow/unfollow and followers/following' do
+ let_it_be(:followee) { create(:user) }
+ let_it_be(:follower) { create(:user) }
+
+ it 'does not show link to follow' do
+ subject
+
+ expect(page).not_to have_link(text: 'Follow', class: 'gl-button')
+ end
+
+ it 'shows 0 followers and 0 following' do
+ subject
+
+ expect(page).to have_content('0 followers')
+ expect(page).to have_content('0 following')
+ end
+
+ it 'shows 1 followers and 1 following' do
+ follower.follow(user)
+ user.follow(followee)
+
+ subject
+
+ expect(page).to have_content('1 follower')
+ expect(page).to have_content('1 following')
+ end
+
+ it 'does show link to follow' do
+ sign_in(user)
+ visit user_path(followee)
+
+ expect(page).to have_link(text: 'Follow', class: 'gl-button')
+ end
+
+ it 'does show link to unfollow' do
+ sign_in(user)
+ user.follow(followee)
+
+ visit user_path(followee)
+
+ expect(page).to have_link(text: 'Unfollow', class: 'gl-button')
+ end
+ end
end
context 'with private profile' do
@@ -83,6 +129,8 @@ RSpec.describe 'User page' do
expect(page).to have_link('Contributed projects')
expect(page).to have_link('Personal projects')
expect(page).to have_link('Snippets')
+ expect(page).to have_link('Followers')
+ expect(page).to have_link('Following')
end
end
end
@@ -242,6 +290,8 @@ RSpec.describe 'User page' do
expect(page).not_to have_link('Contributed projects')
expect(page).not_to have_link('Personal projects')
expect(page).not_to have_link('Snippets')
+ expect(page).not_to have_link('Followers')
+ expect(page).not_to have_link('Following')
end
end
end
@@ -261,6 +311,8 @@ RSpec.describe 'User page' do
expect(page).to have_link('Contributed projects')
expect(page).to have_link('Personal projects')
expect(page).to have_link('Snippets')
+ expect(page).to have_link('Followers')
+ expect(page).to have_link('Following')
end
end
end
diff --git a/spec/features/webauthn_spec.rb b/spec/features/webauthn_spec.rb
index 2ffb6bb3477..4eebc9d2c1e 100644
--- a/spec/features/webauthn_spec.rb
+++ b/spec/features/webauthn_spec.rb
@@ -129,6 +129,10 @@ RSpec.describe 'Using WebAuthn Devices for Authentication', :js do
end
it 'falls back to U2F' do
+ # WebAuthn registration is automatically created with the U2fRegistration because of the after_create callback
+ # so we need to delete it
+ WebauthnRegistration.delete_all
+
gitlab_sign_in(user)
u2f_device.respond_to_u2f_authentication
diff --git a/spec/features/whats_new_spec.rb b/spec/features/whats_new_spec.rb
new file mode 100644
index 00000000000..7c5625486f5
--- /dev/null
+++ b/spec/features/whats_new_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe "renders a `whats new` dropdown item", :js do
+ let_it_be(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'shows notification dot and count and removes it once viewed' do
+ visit root_dashboard_path
+
+ page.within '.header-help' do
+ expect(page).to have_selector('.notification-dot', visible: true)
+
+ find('.header-help-dropdown-toggle').click
+
+ expect(page).to have_button(text: "What's new")
+ expect(page).to have_selector('.js-whats-new-notification-count')
+
+ find('button', text: "What's new").click
+ end
+
+ find('.whats-new-drawer .gl-drawer-close-button').click
+ find('.header-help-dropdown-toggle').click
+
+ page.within '.header-help' do
+ expect(page).not_to have_selector('.notification-dot', visible: true)
+ expect(page).to have_button(text: "What's new")
+ expect(page).not_to have_selector('.js-whats-new-notification-count')
+ end
+ end
+end
diff --git a/spec/finders/autocomplete/users_finder_spec.rb b/spec/finders/autocomplete/users_finder_spec.rb
index 357b6dfcea2..28bd7e12916 100644
--- a/spec/finders/autocomplete/users_finder_spec.rb
+++ b/spec/finders/autocomplete/users_finder_spec.rb
@@ -118,5 +118,10 @@ RSpec.describe Autocomplete::UsersFinder do
it { is_expected.to match_array([user1, external_user, omniauth_user, current_user]) }
end
+
+ it 'preloads the status association' do
+ associations = subject.map { |user| user.association(:status) }
+ expect(associations).to all(be_loaded)
+ end
end
end
diff --git a/spec/finders/ci/jobs_finder_spec.rb b/spec/finders/ci/jobs_finder_spec.rb
index 4a6585e3f2b..ab056dd26e8 100644
--- a/spec/finders/ci/jobs_finder_spec.rb
+++ b/spec/finders/ci/jobs_finder_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Ci::JobsFinder, '#execute' do
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
let_it_be(:job_1) { create(:ci_build) }
let_it_be(:job_2) { create(:ci_build, :running) }
- let_it_be(:job_3) { create(:ci_build, :success, pipeline: pipeline) }
+ let_it_be(:job_3) { create(:ci_build, :success, pipeline: pipeline, name: 'build') }
let(:params) { {} }
@@ -95,4 +95,35 @@ RSpec.describe Ci::JobsFinder, '#execute' do
end
end
end
+
+ context 'when pipeline is present' do
+ before_all do
+ project.add_maintainer(user)
+ job_3.update!(retried: true)
+ end
+
+ let_it_be(:job_4) { create(:ci_build, :success, pipeline: pipeline, name: 'build') }
+
+ subject { described_class.new(current_user: user, pipeline: pipeline, params: params).execute }
+
+ it 'does not return retried jobs by default' do
+ expect(subject).to match_array([job_4])
+ end
+
+ context 'when include_retried is false' do
+ let(:params) { { include_retried: false } }
+
+ it 'does not return retried jobs' do
+ expect(subject).to match_array([job_4])
+ end
+ end
+
+ context 'when include_retried is true' do
+ let(:params) { { include_retried: true } }
+
+ it 'returns retried jobs' do
+ expect(subject).to match_array([job_3, job_4])
+ end
+ end
+ end
end
diff --git a/spec/finders/ci/testing/daily_build_group_report_results_finder_spec.rb b/spec/finders/ci/testing/daily_build_group_report_results_finder_spec.rb
new file mode 100644
index 00000000000..a703f3b800c
--- /dev/null
+++ b/spec/finders/ci/testing/daily_build_group_report_results_finder_spec.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::Testing::DailyBuildGroupReportResultsFinder do
+ describe '#execute' do
+ let_it_be(:project) { create(:project, :private) }
+ let(:user_without_permission) { create(:user) }
+ let_it_be(:user_with_permission) { project.owner }
+ let_it_be(:ref_path) { 'refs/heads/master' }
+ let(:limit) { nil }
+ let_it_be(:default_branch) { false }
+ let(:start_date) { '2020-03-09' }
+ let(:end_date) { '2020-03-10' }
+ let(:sort) { true }
+
+ let_it_be(:rspec_coverage_1) { create_daily_coverage('rspec', 79.0, '2020-03-09') }
+ let_it_be(:karma_coverage_1) { create_daily_coverage('karma', 89.0, '2020-03-09') }
+ let_it_be(:rspec_coverage_2) { create_daily_coverage('rspec', 95.0, '2020-03-10') }
+ let_it_be(:karma_coverage_2) { create_daily_coverage('karma', 92.0, '2020-03-10') }
+ let_it_be(:rspec_coverage_3) { create_daily_coverage('rspec', 97.0, '2020-03-11') }
+ let_it_be(:karma_coverage_3) { create_daily_coverage('karma', 99.0, '2020-03-11') }
+
+ let(:finder) { described_class.new(params: params, current_user: current_user) }
+
+ let(:params) do
+ {
+ project: project,
+ coverage: true,
+ ref_path: ref_path,
+ start_date: start_date,
+ end_date: end_date,
+ limit: limit,
+ sort: sort
+ }
+ end
+
+ subject(:coverages) { finder.execute }
+
+ context 'when params are provided' do
+ context 'when current user is not allowed to read data' do
+ let(:current_user) { user_without_permission }
+
+ it 'returns an empty collection' do
+ expect(coverages).to be_empty
+ end
+ end
+
+ context 'when current user is allowed to read data' do
+ let(:current_user) { user_with_permission }
+
+ it 'returns matching coverages within the given date range' do
+ expect(coverages).to match_array([
+ karma_coverage_2,
+ rspec_coverage_2,
+ karma_coverage_1,
+ rspec_coverage_1
+ ])
+ end
+
+ context 'when ref_path is nil' do
+ let(:default_branch) { true }
+ let(:ref_path) { nil }
+
+ it 'returns coverages for the default branch' do
+ rspec_coverage_4 = create_daily_coverage('rspec', 66.0, '2020-03-10')
+
+ expect(coverages).to contain_exactly(rspec_coverage_4)
+ end
+ end
+
+ context 'when limit is specified' do
+ let(:limit) { 2 }
+
+ it 'returns limited number of matching coverages within the given date range' do
+ expect(coverages).to match_array([
+ karma_coverage_2,
+ rspec_coverage_2
+ ])
+ end
+ end
+ end
+ end
+ end
+
+ private
+
+ def create_daily_coverage(group_name, coverage, date)
+ create(
+ :ci_daily_build_group_report_result,
+ project: project,
+ ref_path: ref_path || 'feature-branch',
+ group_name: group_name,
+ data: { 'coverage' => coverage },
+ date: date,
+ default_branch: default_branch
+ )
+ end
+end
diff --git a/spec/finders/container_repositories_finder_spec.rb b/spec/finders/container_repositories_finder_spec.rb
index b6305e3f5b7..983f6dba28b 100644
--- a/spec/finders/container_repositories_finder_spec.rb
+++ b/spec/finders/container_repositories_finder_spec.rb
@@ -32,6 +32,34 @@ RSpec.describe ContainerRepositoriesFinder do
end
end
+ shared_examples 'with sorting' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:sort_repository) do
+ create(:container_repository, name: 'bar', project: project, created_at: 1.day.ago)
+ end
+
+ let_it_be(:sort_repository2) do
+ create(:container_repository, name: 'foo', project: project, created_at: 1.hour.ago, updated_at: 1.hour.ago)
+ end
+
+ [:created_desc, :updated_asc, :name_desc].each do |order|
+ context "with sort set to #{order}" do
+ let(:params) { { sort: order } }
+
+ it { is_expected.to eq([sort_repository2, sort_repository])}
+ end
+ end
+
+ [:created_asc, :updated_desc, :name_asc].each do |order|
+ context "with sort set to #{order}" do
+ let(:params) { { sort: order } }
+
+ it { is_expected.to eq([sort_repository, sort_repository2])}
+ end
+ end
+ end
+
describe '#execute' do
context 'with authorized user' do
subject { described_class.new(user: reporter, subject: subject_object, params: params).execute }
@@ -47,6 +75,7 @@ RSpec.describe ContainerRepositoriesFinder do
it { is_expected.to match_array([project_repository, other_repository]) }
it_behaves_like 'with name search'
+ it_behaves_like 'with sorting'
end
context 'when subject_type is project' do
@@ -55,6 +84,7 @@ RSpec.describe ContainerRepositoriesFinder do
it { is_expected.to match_array([project_repository]) }
it_behaves_like 'with name search'
+ it_behaves_like 'with sorting'
end
context 'with invalid subject_type' do
diff --git a/spec/finders/deployments_finder_spec.rb b/spec/finders/deployments_finder_spec.rb
index e4e0f366eeb..0f659fa1dab 100644
--- a/spec/finders/deployments_finder_spec.rb
+++ b/spec/finders/deployments_finder_spec.rb
@@ -3,130 +3,161 @@
require 'spec_helper'
RSpec.describe DeploymentsFinder do
- subject { described_class.new(project, params).execute }
-
- let(:project) { create(:project, :public, :test_repo) }
- let(:params) { {} }
+ subject { described_class.new(params).execute }
describe "#execute" do
- it 'returns all deployments by default' do
- deployments = create_list(:deployment, 2, :success, project: project)
- is_expected.to match_array(deployments)
+ context 'when project or group is missing' do
+ let(:params) { {} }
+
+ it 'returns nothing' do
+ is_expected.to eq([])
+ end
end
- describe 'filtering' do
- context 'when updated_at filters are specified' do
- let(:params) { { updated_before: 1.day.ago, updated_after: 3.days.ago } }
- let!(:deployment_1) { create(:deployment, :success, project: project, updated_at: 2.days.ago) }
- let!(:deployment_2) { create(:deployment, :success, project: project, updated_at: 4.days.ago) }
- let!(:deployment_3) { create(:deployment, :success, project: project, updated_at: 1.hour.ago) }
+ context 'at project scope' do
+ let_it_be(:project) { create(:project, :public, :test_repo) }
+ let(:base_params) { { project: project } }
+
+ describe 'filtering' do
+ context 'when updated_at filters are specified' do
+ let(:params) { { **base_params, updated_before: 1.day.ago, updated_after: 3.days.ago } }
+ let!(:deployment_1) { create(:deployment, :success, project: project, updated_at: 2.days.ago) }
+ let!(:deployment_2) { create(:deployment, :success, project: project, updated_at: 4.days.ago) }
+ let!(:deployment_3) { create(:deployment, :success, project: project, updated_at: 1.hour.ago) }
- it 'returns deployments with matched updated_at' do
- is_expected.to match_array([deployment_1])
+ it 'returns deployments with matched updated_at' do
+ is_expected.to match_array([deployment_1])
+ end
end
- end
- context 'when the environment name is specified' do
- let!(:environment1) { create(:environment, project: project) }
- let!(:environment2) { create(:environment, project: project) }
- let!(:deployment1) do
- create(:deployment, project: project, environment: environment1)
+ context 'when the environment name is specified' do
+ let!(:environment1) { create(:environment, project: project) }
+ let!(:environment2) { create(:environment, project: project) }
+ let!(:deployment1) do
+ create(:deployment, project: project, environment: environment1)
+ end
+
+ let!(:deployment2) do
+ create(:deployment, project: project, environment: environment2)
+ end
+
+ let(:params) { { **base_params, environment: environment1.name } }
+
+ it 'returns deployments for the given environment' do
+ is_expected.to match_array([deployment1])
+ end
end
- let!(:deployment2) do
- create(:deployment, project: project, environment: environment2)
+ context 'when the deployment status is specified' do
+ let!(:deployment1) { create(:deployment, :success, project: project) }
+ let!(:deployment2) { create(:deployment, :failed, project: project) }
+ let(:params) { { **base_params, status: 'success' } }
+
+ it 'returns deployments for the given environment' do
+ is_expected.to match_array([deployment1])
+ end
end
- let(:params) { { environment: environment1.name } }
+ context 'when using an invalid deployment status' do
+ let(:params) { { **base_params, status: 'kittens' } }
- it 'returns deployments for the given environment' do
- is_expected.to match_array([deployment1])
+ it 'raises ArgumentError' do
+ expect { subject }.to raise_error(ArgumentError)
+ end
end
end
- context 'when the deployment status is specified' do
- let!(:deployment1) { create(:deployment, :success, project: project) }
- let!(:deployment2) { create(:deployment, :failed, project: project) }
- let(:params) { { status: 'success' } }
+ describe 'ordering' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:params) { { **base_params, order_by: order_by, sort: sort } }
+
+ let!(:deployment_1) { create(:deployment, :success, project: project, iid: 11, ref: 'master', created_at: 2.days.ago, updated_at: Time.now, finished_at: Time.now) }
+ let!(:deployment_2) { create(:deployment, :success, project: project, iid: 12, ref: 'feature', created_at: 1.day.ago, updated_at: 2.hours.ago, finished_at: 2.hours.ago) }
+ let!(:deployment_3) { create(:deployment, :success, project: project, iid: 8, ref: 'video', created_at: Time.now, updated_at: 1.hour.ago, finished_at: 1.hour.ago) }
+
+ where(:order_by, :sort, :ordered_deployments) do
+ 'created_at' | 'asc' | [:deployment_1, :deployment_2, :deployment_3]
+ 'created_at' | 'desc' | [:deployment_3, :deployment_2, :deployment_1]
+ 'id' | 'asc' | [:deployment_1, :deployment_2, :deployment_3]
+ 'id' | 'desc' | [:deployment_3, :deployment_2, :deployment_1]
+ 'iid' | 'asc' | [:deployment_3, :deployment_1, :deployment_2]
+ 'iid' | 'desc' | [:deployment_2, :deployment_1, :deployment_3]
+ 'ref' | 'asc' | [:deployment_2, :deployment_1, :deployment_3]
+ 'ref' | 'desc' | [:deployment_3, :deployment_1, :deployment_2]
+ 'updated_at' | 'asc' | [:deployment_2, :deployment_3, :deployment_1]
+ 'updated_at' | 'desc' | [:deployment_1, :deployment_3, :deployment_2]
+ 'finished_at' | 'asc' | [:deployment_2, :deployment_3, :deployment_1]
+ 'finished_at' | 'desc' | [:deployment_1, :deployment_3, :deployment_2]
+ 'invalid' | 'asc' | [:deployment_1, :deployment_2, :deployment_3]
+ 'iid' | 'err' | [:deployment_3, :deployment_1, :deployment_2]
+ end
- it 'returns deployments for the given environment' do
- is_expected.to match_array([deployment1])
+ with_them do
+ it 'returns the deployments ordered' do
+ expect(subject).to eq(ordered_deployments.map { |name| public_send(name) })
+ end
end
end
- context 'when using an invalid deployment status' do
- let(:params) { { status: 'kittens' } }
+ describe 'transform `created_at` sorting to `id` sorting' do
+ let(:params) { { **base_params, order_by: 'created_at', sort: 'asc' } }
- it 'raises ArgumentError' do
- expect { subject }.to raise_error(ArgumentError)
+ it 'sorts by only one column' do
+ expect(subject.order_values.size).to eq(1)
end
- end
- end
- describe 'ordering' do
- using RSpec::Parameterized::TableSyntax
-
- let(:params) { { order_by: order_by, sort: sort } }
-
- let!(:deployment_1) { create(:deployment, :success, project: project, iid: 11, ref: 'master', created_at: 2.days.ago, updated_at: Time.now) }
- let!(:deployment_2) { create(:deployment, :success, project: project, iid: 12, ref: 'feature', created_at: 1.day.ago, updated_at: 2.hours.ago) }
- let!(:deployment_3) { create(:deployment, :success, project: project, iid: 8, ref: 'video', created_at: Time.now, updated_at: 1.hour.ago) }
-
- where(:order_by, :sort, :ordered_deployments) do
- 'created_at' | 'asc' | [:deployment_1, :deployment_2, :deployment_3]
- 'created_at' | 'desc' | [:deployment_3, :deployment_2, :deployment_1]
- 'id' | 'asc' | [:deployment_1, :deployment_2, :deployment_3]
- 'id' | 'desc' | [:deployment_3, :deployment_2, :deployment_1]
- 'iid' | 'asc' | [:deployment_3, :deployment_1, :deployment_2]
- 'iid' | 'desc' | [:deployment_2, :deployment_1, :deployment_3]
- 'ref' | 'asc' | [:deployment_2, :deployment_1, :deployment_3]
- 'ref' | 'desc' | [:deployment_3, :deployment_1, :deployment_2]
- 'updated_at' | 'asc' | [:deployment_2, :deployment_3, :deployment_1]
- 'updated_at' | 'desc' | [:deployment_1, :deployment_3, :deployment_2]
- 'invalid' | 'asc' | [:deployment_1, :deployment_2, :deployment_3]
- 'iid' | 'err' | [:deployment_3, :deployment_1, :deployment_2]
+ it 'sorts by `id`' do
+ expect(subject.order_values.first.to_sql).to eq(Deployment.arel_table[:id].asc.to_sql)
+ end
end
- with_them do
- it 'returns the deployments ordered' do
- expect(subject).to eq(ordered_deployments.map { |name| public_send(name) })
+ describe 'tie-breaker for `finished_at` sorting' do
+ let(:params) { { **base_params, order_by: 'updated_at', sort: 'asc' } }
+
+ it 'sorts by two columns' do
+ expect(subject.order_values.size).to eq(2)
end
- end
- end
- describe 'transform `created_at` sorting to `id` sorting' do
- let(:params) { { order_by: 'created_at', sort: 'asc' } }
+ it 'adds `id` sorting as the second order column' do
+ order_value = subject.order_values[1]
- it 'sorts by only one column' do
- expect(subject.order_values.size).to eq(1)
- end
+ expect(order_value.to_sql).to eq(Deployment.arel_table[:id].desc.to_sql)
+ end
- it 'sorts by `id`' do
- expect(subject.order_values.first.to_sql).to eq(Deployment.arel_table[:id].asc.to_sql)
- end
- end
+ it 'uses the `id DESC` as tie-breaker when ordering' do
+ updated_at = Time.now
- describe 'tie-breaker for `updated_at` sorting' do
- let(:params) { { order_by: 'updated_at', sort: 'asc' } }
+ deployment_1 = create(:deployment, :success, project: project, updated_at: updated_at)
+ deployment_2 = create(:deployment, :success, project: project, updated_at: updated_at)
+ deployment_3 = create(:deployment, :success, project: project, updated_at: updated_at)
- it 'sorts by two columns' do
- expect(subject.order_values.size).to eq(2)
+ expect(subject).to eq([deployment_3, deployment_2, deployment_1])
+ end
end
- it 'adds `id` sorting as the second order column' do
- order_value = subject.order_values[1]
+ context 'when filtering by finished time' do
+ let!(:deployment_1) { create(:deployment, :success, project: project, finished_at: 2.days.ago) }
+ let!(:deployment_2) { create(:deployment, :success, project: project, finished_at: 4.days.ago) }
+ let!(:deployment_3) { create(:deployment, :success, project: project, finished_at: 5.hours.ago) }
- expect(order_value.to_sql).to eq(Deployment.arel_table[:id].desc.to_sql)
- end
+ context 'when filtering by finished_after and finished_before' do
+ let(:params) { { **base_params, finished_after: 3.days.ago, finished_before: 1.day.ago } }
+
+ it { is_expected.to match_array([deployment_1]) }
+ end
- it 'uses the `id DESC` as tie-breaker when ordering' do
- updated_at = Time.now
+ context 'when the finished_before parameter is missing' do
+ let(:params) { { **base_params, finished_after: 3.days.ago } }
- deployment_1 = create(:deployment, :success, project: project, updated_at: updated_at)
- deployment_2 = create(:deployment, :success, project: project, updated_at: updated_at)
- deployment_3 = create(:deployment, :success, project: project, updated_at: updated_at)
+ it { is_expected.to match_array([deployment_1, deployment_3]) }
+ end
+
+ context 'when finished_after is missing' do
+ let(:params) { { **base_params, finished_before: 3.days.ago } }
- expect(subject).to eq([deployment_3, deployment_2, deployment_1])
+ it { is_expected.to match_array([deployment_2]) }
+ end
end
end
end
diff --git a/spec/finders/license_template_finder_spec.rb b/spec/finders/license_template_finder_spec.rb
index 93f13632b6f..754b92faccc 100644
--- a/spec/finders/license_template_finder_spec.rb
+++ b/spec/finders/license_template_finder_spec.rb
@@ -3,12 +3,7 @@
require 'spec_helper'
RSpec.describe LicenseTemplateFinder do
- describe '#execute' do
- subject(:result) { described_class.new(nil, params).execute }
-
- let(:categories) { categorised_licenses.keys }
- let(:categorised_licenses) { result.group_by(&:category) }
-
+ RSpec.shared_examples 'filters by popular category' do
context 'popular: true' do
let(:params) { { popular: true } }
@@ -26,6 +21,15 @@ RSpec.describe LicenseTemplateFinder do
expect(categorised_licenses[:Other]).to be_present
end
end
+ end
+
+ describe '#execute' do
+ subject(:result) { described_class.new(nil, params).execute }
+
+ let(:categories) { categorised_licenses.keys }
+ let(:categorised_licenses) { result.group_by(&:category) }
+
+ it_behaves_like 'filters by popular category'
context 'popular: nil' do
let(:params) { { popular: nil } }
@@ -48,4 +52,31 @@ RSpec.describe LicenseTemplateFinder do
end
end
end
+
+ describe '#template_names' do
+ let(:params) { {} }
+
+ subject(:template_names) { described_class.new(nil, params).template_names }
+
+ let(:categories) { categorised_licenses.keys }
+ let(:categorised_licenses) { template_names }
+
+ it_behaves_like 'filters by popular category'
+
+ context 'popular: nil' do
+ let(:params) { { popular: nil } }
+
+ it 'returns all licenses known by the Licensee gem' do
+ from_licensee = Licensee::License.all.map { |l| l.key }
+
+ expect(template_names.values.flatten.map { |x| x[:key] }).to match_array(from_licensee)
+ end
+ end
+
+ context 'template names hash keys' do
+ it 'has all the expected keys' do
+ expect(template_names.values.flatten.first.keys).to match_array(%i(id key name project_id))
+ end
+ end
+ end
end
diff --git a/spec/finders/merge_request/metrics_finder_spec.rb b/spec/finders/merge_request/metrics_finder_spec.rb
new file mode 100644
index 00000000000..ea039462e66
--- /dev/null
+++ b/spec/finders/merge_request/metrics_finder_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe MergeRequest::MetricsFinder do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:merge_request_not_merged) { create(:merge_request, :unique_branches, source_project: project) }
+ let_it_be(:merged_at) { Time.new(2020, 5, 1) }
+ let_it_be(:merge_request_merged) do
+ create(:merge_request, :unique_branches, :merged, source_project: project).tap do |mr|
+ mr.metrics.update!(merged_at: merged_at)
+ end
+ end
+
+ let(:params) do
+ {
+ target_project: project,
+ merged_after: merged_at - 10.days,
+ merged_before: merged_at + 10.days
+ }
+ end
+
+ subject { described_class.new(current_user, params).execute.to_a }
+
+ context 'when target project is missing' do
+ before do
+ params.delete(:target_project)
+ end
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'when the user is not part of the project' do
+ it { is_expected.to be_empty }
+ end
+
+ context 'when user is part of the project' do
+ before do
+ project.add_developer(current_user)
+ end
+
+ it 'returns merge request records' do
+ is_expected.to eq([merge_request_merged.metrics])
+ end
+
+ it 'excludes not merged records' do
+ is_expected.not_to eq([merge_request_not_merged.metrics])
+ end
+
+ context 'when only merged_before is given' do
+ before do
+ params.delete(:merged_after)
+ end
+
+ it { is_expected.to eq([merge_request_merged.metrics]) }
+ end
+
+ context 'when only merged_after is given' do
+ before do
+ params.delete(:merged_before)
+ end
+
+ it { is_expected.to eq([merge_request_merged.metrics]) }
+ end
+
+ context 'when no records matching the date range' do
+ before do
+ params[:merged_before] = merged_at - 1.year
+ params[:merged_after] = merged_at - 2.years
+ end
+
+ it { is_expected.to be_empty }
+ end
+ end
+end
diff --git a/spec/finders/merge_requests/oldest_per_commit_finder_spec.rb b/spec/finders/merge_requests/oldest_per_commit_finder_spec.rb
new file mode 100644
index 00000000000..4e9d021fa5d
--- /dev/null
+++ b/spec/finders/merge_requests/oldest_per_commit_finder_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe MergeRequests::OldestPerCommitFinder do
+ describe '#execute' do
+ it 'returns a Hash mapping commit SHAs to their oldest merge requests' do
+ project = create(:project)
+ mr1 = create(:merge_request, :merged, target_project: project)
+ mr2 = create(:merge_request, :merged, target_project: project)
+ mr1_diff = create(:merge_request_diff, merge_request: mr1)
+ mr2_diff = create(:merge_request_diff, merge_request: mr2)
+ sha1 = Digest::SHA1.hexdigest('foo')
+ sha2 = Digest::SHA1.hexdigest('bar')
+
+ create(:merge_request_diff_commit, merge_request_diff: mr1_diff, sha: sha1)
+ create(:merge_request_diff_commit, merge_request_diff: mr2_diff, sha: sha1)
+ create(
+ :merge_request_diff_commit,
+ merge_request_diff: mr2_diff,
+ sha: sha2,
+ relative_order: 1
+ )
+
+ commits = [double(:commit, id: sha1), double(:commit, id: sha2)]
+
+ expect(described_class.new(project).execute(commits)).to eq(
+ sha1 => mr1,
+ sha2 => mr2
+ )
+ end
+
+ it 'skips merge requests that are not merged' do
+ mr = create(:merge_request)
+ mr_diff = create(:merge_request_diff, merge_request: mr)
+ sha = Digest::SHA1.hexdigest('foo')
+
+ create(:merge_request_diff_commit, merge_request_diff: mr_diff, sha: sha)
+
+ commits = [double(:commit, id: sha)]
+
+ expect(described_class.new(mr.target_project).execute(commits))
+ .to be_empty
+ end
+ end
+end
diff --git a/spec/finders/packages/group_packages_finder_spec.rb b/spec/finders/packages/group_packages_finder_spec.rb
index 8dd53b9c3f9..445482a5a96 100644
--- a/spec/finders/packages/group_packages_finder_spec.rb
+++ b/spec/finders/packages/group_packages_finder_spec.rb
@@ -147,6 +147,7 @@ RSpec.describe Packages::GroupPackagesFinder do
end
it_behaves_like 'concerning versionless param'
+ it_behaves_like 'concerning package statuses'
end
context 'group has package of all types' do
diff --git a/spec/finders/packages/packages_finder_spec.rb b/spec/finders/packages/packages_finder_spec.rb
index 77a171db144..6e92616bafa 100644
--- a/spec/finders/packages/packages_finder_spec.rb
+++ b/spec/finders/packages/packages_finder_spec.rb
@@ -82,5 +82,6 @@ RSpec.describe ::Packages::PackagesFinder do
end
it_behaves_like 'concerning versionless param'
+ it_behaves_like 'concerning package statuses'
end
end
diff --git a/spec/finders/repositories/commits_with_trailer_finder_spec.rb b/spec/finders/repositories/commits_with_trailer_finder_spec.rb
new file mode 100644
index 00000000000..0c457aae340
--- /dev/null
+++ b/spec/finders/repositories/commits_with_trailer_finder_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Repositories::CommitsWithTrailerFinder do
+ let(:project) { create(:project, :repository) }
+
+ describe '#each_page' do
+ it 'only yields commits with the given trailer' do
+ finder = described_class.new(
+ project: project,
+ from: '570e7b2abdd848b95f2f578043fc23bd6f6fd24d',
+ to: 'c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd'
+ )
+
+ commits = finder.each_page('Signed-off-by').to_a.flatten
+
+ expect(commits.length).to eq(1)
+ expect(commits.first.id).to eq('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
+ expect(commits.first.trailers).to eq(
+ 'Signed-off-by' => 'Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>'
+ )
+ end
+
+ it 'supports paginating of commits' do
+ finder = described_class.new(
+ project: project,
+ from: 'c1acaa58bbcbc3eafe538cb8274ba387047b69f8',
+ to: '5937ac0a7beb003549fc5fd26fc247adbce4a52e',
+ per_page: 1
+ )
+
+ commits = finder.each_page('Signed-off-by')
+
+ expect(commits.count).to eq(4)
+ end
+ end
+end
diff --git a/spec/finders/repositories/previous_tag_finder_spec.rb b/spec/finders/repositories/previous_tag_finder_spec.rb
new file mode 100644
index 00000000000..7cc33d11baf
--- /dev/null
+++ b/spec/finders/repositories/previous_tag_finder_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Repositories::PreviousTagFinder do
+ let(:project) { build_stubbed(:project) }
+ let(:finder) { described_class.new(project) }
+
+ describe '#execute' do
+ context 'when there is a previous tag' do
+ it 'returns the previous tag' do
+ tag1 = double(:tag1, name: 'v1.0.0')
+ tag2 = double(:tag2, name: 'v1.1.0')
+ tag3 = double(:tag3, name: 'v2.0.0')
+ tag4 = double(:tag4, name: '1.0.0')
+
+ allow(project.repository)
+ .to receive(:tags)
+ .and_return([tag1, tag3, tag2, tag4])
+
+ expect(finder.execute('2.1.0')).to eq(tag3)
+ expect(finder.execute('2.0.0')).to eq(tag2)
+ expect(finder.execute('1.5.0')).to eq(tag2)
+ expect(finder.execute('1.0.1')).to eq(tag1)
+ end
+ end
+
+ context 'when there is no previous tag' do
+ it 'returns nil' do
+ tag1 = double(:tag1, name: 'v1.0.0')
+ tag2 = double(:tag2, name: 'v1.1.0')
+
+ allow(project.repository)
+ .to receive(:tags)
+ .and_return([tag1, tag2])
+
+ expect(finder.execute('1.0.0')).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/finders/template_finder_spec.rb b/spec/finders/template_finder_spec.rb
index 2da864b9a46..164975fdfb6 100644
--- a/spec/finders/template_finder_spec.rb
+++ b/spec/finders/template_finder_spec.rb
@@ -5,6 +5,102 @@ require 'spec_helper'
RSpec.describe TemplateFinder do
using RSpec::Parameterized::TableSyntax
+ let_it_be(:template_files) do
+ {
+ "Dockerfile/project_dockerfiles_template.dockerfile" => "project_dockerfiles_template content",
+ "gitignore/project_gitignores_template.gitignore" => "project_gitignores_template content",
+ "gitlab-ci/project_gitlab_ci_ymls_template.yml" => "project_gitlab_ci_ymls_template content",
+ ".gitlab/issue_templates/project_issues_template.md" => "project_issues_template content",
+ ".gitlab/merge_request_templates/project_merge_requests_template.md" => "project_merge_requests_template content"
+ }
+ end
+
+ RSpec.shared_examples 'fetches predefined vendor templates' do
+ where(:type, :vendored_name) do
+ :dockerfiles | 'Binary'
+ :gitignores | 'Actionscript'
+ :gitlab_ci_ymls | 'Android'
+ :metrics_dashboard_ymls | 'Default'
+ :gitlab_ci_syntax_ymls | 'Artifacts example'
+ end
+
+ with_them do
+ it 'returns all vendored templates when no name is specified' do
+ expect(result).to include(have_attributes(name: vendored_name))
+ end
+
+ context 'with name param' do
+ let(:params) { { name: vendored_name } }
+
+ it 'returns only the specified vendored template when a name is specified' do
+ expect(result).to have_attributes(name: vendored_name)
+ end
+
+ context 'with mistaken name param' do
+ let(:params) { { name: 'unknown' } }
+
+ it 'returns nil when an unknown name is specified' do
+ expect(result).to be_nil
+ end
+ end
+ end
+ end
+ end
+
+ RSpec.shared_examples 'no issues and merge requests templates available' do
+ context 'with issue and merge request templates' do
+ where(:type, :vendored_name) do
+ :issues | nil
+ :merge_requests | nil
+ end
+
+ with_them do
+ context 'when fetching all templates' do
+ it 'returns empty array' do
+ expect(result).to eq([])
+ end
+ end
+
+ context 'when looking for specific template by name' do
+ let(:params) { { name: 'anything' } }
+
+ it 'raises an error' do
+ expect { result }.to raise_exception(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
+ end
+ end
+ end
+ end
+ end
+
+ RSpec.shared_examples 'fetches issues and merge requests templates' do
+ where(:type, :template_name) do
+ :issues | 'project_issues_template'
+ :merge_requests | 'project_merge_requests_template'
+ end
+
+ with_them do
+ it 'returns all repository template files for issues and merge requests' do
+ expect(result).to include(have_attributes(name: template_name))
+ end
+
+ context 'with name param' do
+ let(:params) { { name: template_name } }
+
+ it 'returns only the specified vendored template when a name is specified' do
+ expect(result).to have_attributes(name: template_name)
+ end
+
+ context 'with mistaken name param' do
+ let(:params) { { name: 'unknown' } }
+
+ it 'raises an error when an unknown name is specified' do
+ expect { result }.to raise_exception(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
+ end
+ end
+ end
+ end
+ end
+
describe '#build' do
let(:project) { build_stubbed(:project) }
@@ -15,6 +111,8 @@ RSpec.describe TemplateFinder do
:licenses | ::LicenseTemplateFinder
:metrics_dashboard_ymls | described_class
:gitlab_ci_syntax_ymls | described_class
+ :issues | described_class
+ :merge_requests | described_class
end
with_them do
@@ -26,6 +124,37 @@ RSpec.describe TemplateFinder do
end
describe '#execute' do
+ let_it_be(:project) { nil }
+ let(:params) { {} }
+
+ subject(:result) { described_class.new(type, project, params).execute }
+
+ context 'when no project is passed in' do
+ it_behaves_like 'fetches predefined vendor templates'
+ it_behaves_like 'no issues and merge requests templates available'
+ end
+
+ context 'when project has no repository' do
+ let_it_be(:project) { create(:project) }
+
+ it_behaves_like 'fetches predefined vendor templates'
+ it_behaves_like 'no issues and merge requests templates available'
+ end
+
+ context 'when project has a repository' do
+ let_it_be(:project) { create(:project, :custom_repo, files: template_files) }
+
+ it_behaves_like 'fetches predefined vendor templates'
+ it_behaves_like 'fetches issues and merge requests templates'
+ end
+ end
+
+ describe '#template_names' do
+ let_it_be(:project) { nil }
+ let(:params) { {} }
+
+ subject(:result) { described_class.new(type, project, params).template_names.values.flatten.map { |el| OpenStruct.new(el) } }
+
where(:type, :vendored_name) do
:dockerfiles | 'Binary'
:gitignores | 'Actionscript'
@@ -35,22 +164,67 @@ RSpec.describe TemplateFinder do
end
with_them do
- it 'returns all vendored templates when no name is specified' do
- result = described_class.new(type, nil).execute
+ context 'when no project is passed in' do
+ it 'returns all vendored templates when no name is specified' do
+ expect(result).to include(have_attributes(name: vendored_name))
+ end
+ end
- expect(result).to include(have_attributes(name: vendored_name))
+ context 'when project has no repository' do
+ let_it_be(:project) { create(:project) }
+
+ it 'returns all vendored templates when no name is specified' do
+ expect(result).to include(have_attributes(name: vendored_name))
+ end
end
- it 'returns only the specified vendored template when a name is specified' do
- result = described_class.new(type, nil, name: vendored_name).execute
+ context 'when project has a repository' do
+ let_it_be(:project) { create(:project, :custom_repo, files: template_files) }
- expect(result).to have_attributes(name: vendored_name)
+ it 'returns all vendored templates when no name is specified' do
+ expect(result).to include(have_attributes(name: vendored_name))
+ end
end
- it 'returns nil when an unknown name is specified' do
- result = described_class.new(type, nil, name: 'unknown').execute
+ context 'template names hash keys' do
+ it 'has all the expected keys' do
+ expect(result.first.to_h.keys).to match_array(%i(id key name project_id))
+ end
+ end
+ end
+
+ where(:type, :template_name) do
+ :issues | 'project_issues_template'
+ :merge_requests | 'project_merge_requests_template'
+ end
+
+ with_them do
+ context 'when no project is passed in' do
+ it 'returns all vendored templates when no name is specified' do
+ expect(result).to eq([])
+ end
+ end
+
+ context 'when project has no repository' do
+ let_it_be(:project) { create(:project) }
+
+ it 'returns all vendored templates when no name is specified' do
+ expect(result).to eq([])
+ end
+ end
+
+ context 'when project has a repository' do
+ let_it_be(:project) { create(:project, :custom_repo, files: template_files) }
+
+ it 'returns all vendored templates when no name is specified' do
+ expect(result).to include(have_attributes(name: template_name))
+ end
- expect(result).to be_nil
+ context 'template names hash keys' do
+ it 'has all the expected keys' do
+ expect(result.first.to_h.keys).to match_array(%i(id key name project_id))
+ end
+ end
end
end
end
diff --git a/spec/finders/terraform/states_finder_spec.rb b/spec/finders/terraform/states_finder_spec.rb
new file mode 100644
index 00000000000..260e5f4818f
--- /dev/null
+++ b/spec/finders/terraform/states_finder_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Terraform::StatesFinder do
+ describe '#execute' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:state_1) { create(:terraform_state, project: project) }
+ let_it_be(:state_2) { create(:terraform_state, project: project) }
+
+ let(:user) { project.creator }
+
+ subject { described_class.new(project, user).execute }
+
+ it { is_expected.to contain_exactly(state_1, state_2) }
+
+ context 'user does not have permission' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_guest(user)
+ end
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'filtering by name' do
+ let(:params) { { name: name_param } }
+
+ subject { described_class.new(project, user, params: params).execute }
+
+ context 'name does not match' do
+ let(:name_param) { 'other-name' }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'name does match' do
+ let(:name_param) { state_1.name }
+
+ it { is_expected.to contain_exactly(state_1) }
+ end
+ end
+ end
+end
diff --git a/spec/finders/user_recent_events_finder_spec.rb b/spec/finders/user_recent_events_finder_spec.rb
index ddba9b595a4..5a9243d150d 100644
--- a/spec/finders/user_recent_events_finder_spec.rb
+++ b/spec/finders/user_recent_events_finder_spec.rb
@@ -5,16 +5,17 @@ require 'spec_helper'
RSpec.describe UserRecentEventsFinder do
let_it_be(:project_owner, reload: true) { create(:user) }
let_it_be(:current_user, reload: true) { create(:user) }
- let(:private_project) { create(:project, :private, creator: project_owner) }
- let(:internal_project) { create(:project, :internal, creator: project_owner) }
- let(:public_project) { create(:project, :public, creator: project_owner) }
+ let_it_be(:private_project) { create(:project, :private, creator: project_owner) }
+ let_it_be(:internal_project) { create(:project, :internal, creator: project_owner) }
+ let_it_be(:public_project) { create(:project, :public, creator: project_owner) }
let!(:private_event) { create(:event, project: private_project, author: project_owner) }
let!(:internal_event) { create(:event, project: internal_project, author: project_owner) }
let!(:public_event) { create(:event, project: public_project, author: project_owner) }
+ let_it_be(:issue) { create(:issue, project: public_project) }
let(:limit) { nil }
let(:params) { { limit: limit } }
- subject(:finder) { described_class.new(current_user, project_owner, params) }
+ subject(:finder) { described_class.new(current_user, project_owner, nil, params) }
describe '#execute' do
context 'when profile is public' do
@@ -39,11 +40,116 @@ RSpec.describe UserRecentEventsFinder do
expect(finder.execute).to be_empty
end
- describe 'design activity events' do
- let_it_be(:event_a) { create(:design_event, author: project_owner) }
- let_it_be(:event_b) { create(:design_event, author: project_owner) }
+ context 'events from multiple users' do
+ let_it_be(:second_user, reload: true) { create(:user) }
+ let_it_be(:private_project_second_user) { create(:project, :private, creator: second_user) }
+ let(:internal_project_second_user) { create(:project, :internal, creator: second_user) }
+ let(:public_project_second_user) { create(:project, :public, creator: second_user) }
+ let!(:private_event_second_user) { create(:event, project: private_project_second_user, author: second_user) }
+ let!(:internal_event_second_user) { create(:event, project: internal_project_second_user, author: second_user) }
+ let!(:public_event_second_user) { create(:event, project: public_project_second_user, author: second_user) }
+
+ it 'includes events from all users', :aggregate_failures do
+ events = described_class.new(current_user, [project_owner, second_user], nil, params).execute
+
+ expect(events).to include(private_event, internal_event, public_event)
+ expect(events).to include(private_event_second_user, internal_event_second_user, public_event_second_user)
+ expect(events.size).to eq(6)
+ end
+
+ it 'does not include events from users with private profile', :aggregate_failures do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?).with(current_user, :read_user_profile, second_user).and_return(false)
+
+ events = described_class.new(current_user, [project_owner, second_user], nil, params).execute
+
+ expect(events).to include(private_event, internal_event, public_event)
+ expect(events.size).to eq(3)
+ end
+ end
+
+ context 'filter activity events' do
+ let!(:push_event) { create(:push_event, project: public_project, author: project_owner) }
+ let!(:merge_event) { create(:event, :merged, project: public_project, author: project_owner) }
+ let!(:issue_event) { create(:event, :closed, project: public_project, target: issue, author: project_owner) }
+ let!(:comment_event) { create(:event, :commented, project: public_project, author: project_owner) }
+ let!(:wiki_event) { create(:wiki_page_event, project: public_project, author: project_owner) }
+ let!(:design_event) { create(:design_event, project: public_project, author: project_owner) }
+ let!(:team_event) { create(:event, :joined, project: public_project, author: project_owner) }
+
+ it 'includes all events', :aggregate_failures do
+ event_filter = EventFilter.new(EventFilter::ALL)
+ events = described_class.new(current_user, project_owner, event_filter, params).execute
+
+ expect(events).to include(private_event, internal_event, public_event)
+ expect(events).to include(push_event, merge_event, issue_event, comment_event, wiki_event, design_event, team_event)
+ expect(events.size).to eq(10)
+ end
+
+ it 'only includes push events', :aggregate_failures do
+ event_filter = EventFilter.new(EventFilter::PUSH)
+ events = described_class.new(current_user, project_owner, event_filter, params).execute
+
+ expect(events).to include(push_event)
+ expect(events.size).to eq(1)
+ end
+
+ it 'only includes merge events', :aggregate_failures do
+ event_filter = EventFilter.new(EventFilter::MERGED)
+ events = described_class.new(current_user, project_owner, event_filter, params).execute
+
+ expect(events).to include(merge_event)
+ expect(events.size).to eq(1)
+ end
+
+ it 'only includes issue events', :aggregate_failures do
+ event_filter = EventFilter.new(EventFilter::ISSUE)
+ events = described_class.new(current_user, project_owner, event_filter, params).execute
+
+ expect(events).to include(issue_event)
+ expect(events.size).to eq(1)
+ end
+
+ it 'only includes comments events', :aggregate_failures do
+ event_filter = EventFilter.new(EventFilter::COMMENTS)
+ events = described_class.new(current_user, project_owner, event_filter, params).execute
+
+ expect(events).to include(comment_event)
+ expect(events.size).to eq(1)
+ end
+
+ it 'only includes wiki events', :aggregate_failures do
+ event_filter = EventFilter.new(EventFilter::WIKI)
+ events = described_class.new(current_user, project_owner, event_filter, params).execute
+
+ expect(events).to include(wiki_event)
+ expect(events.size).to eq(1)
+ end
it 'only includes design events', :aggregate_failures do
+ event_filter = EventFilter.new(EventFilter::DESIGNS)
+ events = described_class.new(current_user, project_owner, event_filter, params).execute
+
+ expect(events).to include(design_event)
+ expect(events.size).to eq(1)
+ end
+
+ it 'only includes team events', :aggregate_failures do
+ event_filter = EventFilter.new(EventFilter::TEAM)
+ events = described_class.new(current_user, project_owner, event_filter, params).execute
+
+ expect(events).to include(private_event, internal_event, public_event, team_event)
+ expect(events.size).to eq(4)
+ end
+ end
+
+ describe 'issue activity events' do
+ let(:issue) { create(:issue, project: public_project) }
+ let(:note) { create(:note_on_issue, noteable: issue, project: public_project) }
+ let!(:event_a) { create(:event, :commented, target: note, author: project_owner) }
+ let!(:event_b) { create(:event, :closed, target: issue, author: project_owner) }
+
+ it 'includes all issue related events', :aggregate_failures do
events = finder.execute
expect(events).to include(event_a)
diff --git a/spec/fixtures/api/schemas/entities/codequality_mr_diff_report.json b/spec/fixtures/api/schemas/entities/codequality_mr_diff_report.json
new file mode 100644
index 00000000000..63e0c68e9cd
--- /dev/null
+++ b/spec/fixtures/api/schemas/entities/codequality_mr_diff_report.json
@@ -0,0 +1,21 @@
+{
+ "type": "object",
+ "description": "The schema used to display codequality report in mr diff",
+ "required": ["files"],
+ "properties": {
+ "patternProperties": {
+ ".*.": {
+ "type": "array",
+ "items": {
+ "required": ["line", "description", "severity"],
+ "properties": {
+ "line": { "type": "integer" },
+ "description": { "type": "string" },
+ "severity": { "type": "string" }
+ },
+ "additionalProperties": false
+ }
+ }
+ }
+ }
+}
diff --git a/spec/fixtures/api/schemas/entities/member.json b/spec/fixtures/api/schemas/entities/member.json
index e8b40745803..03b1872632e 100644
--- a/spec/fixtures/api/schemas/entities/member.json
+++ b/spec/fixtures/api/schemas/entities/member.json
@@ -9,7 +9,8 @@
"source",
"valid_roles",
"can_update",
- "can_remove"
+ "can_remove",
+ "is_direct_member"
],
"properties": {
"id": { "type": "integer" },
@@ -18,6 +19,7 @@
"requested_at": { "type": ["date-time", "null"] },
"can_update": { "type": "boolean" },
"can_remove": { "type": "boolean" },
+ "is_direct_member": { "type": "boolean" },
"access_level": {
"type": "object",
"required": ["integer_value", "string_value"],
diff --git a/spec/fixtures/api/schemas/entities/member_user.json b/spec/fixtures/api/schemas/entities/member_user.json
index 983cdb7b9d9..ebd26bfaaaa 100644
--- a/spec/fixtures/api/schemas/entities/member_user.json
+++ b/spec/fixtures/api/schemas/entities/member_user.json
@@ -9,6 +9,7 @@
"web_url": { "type": "string" },
"blocked": { "type": "boolean" },
"two_factor_enabled": { "type": "boolean" },
+ "availability": { "type": ["string", "null"] },
"status": {
"type": "object",
"required": ["emoji"],
diff --git a/spec/fixtures/api/schemas/graphql/packages/package_composer_details.json b/spec/fixtures/api/schemas/graphql/packages/package_composer_details.json
deleted file mode 100644
index bcf64a6e567..00000000000
--- a/spec/fixtures/api/schemas/graphql/packages/package_composer_details.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "type": "object",
- "allOf": [{ "$ref": "./package_details.json" }],
- "properties": {
- "target_sha": {
- "type": "string"
- },
- "composer_json": {
- "type": "object"
- }
- }
-}
diff --git a/spec/fixtures/api/schemas/graphql/packages/package_composer_metadata.json b/spec/fixtures/api/schemas/graphql/packages/package_composer_metadata.json
new file mode 100644
index 00000000000..db9b25889be
--- /dev/null
+++ b/spec/fixtures/api/schemas/graphql/packages/package_composer_metadata.json
@@ -0,0 +1,21 @@
+{
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["targetSha", "composerJson"],
+ "properties": {
+ "targetSha": {
+ "type": "string"
+ },
+ "composerJson": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["name", "type", "license", "version"],
+ "properties": {
+ "name": { "type": "string" },
+ "type": { "type": "string" },
+ "license": { "type": "string" },
+ "version": { "type": "string" }
+ }
+ }
+ }
+}
diff --git a/spec/fixtures/api/schemas/graphql/packages/package_details.json b/spec/fixtures/api/schemas/graphql/packages/package_details.json
index 4f90285183c..d2e2e65db54 100644
--- a/spec/fixtures/api/schemas/graphql/packages/package_details.json
+++ b/spec/fixtures/api/schemas/graphql/packages/package_details.json
@@ -1,5 +1,10 @@
{
"type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id", "name", "createdAt", "updatedAt", "version", "packageType",
+ "project", "tags", "pipelines", "versions", "metadata"
+ ],
"properties": {
"id": {
"type": "string"
@@ -16,21 +21,46 @@
"version": {
"type": ["string", "null"]
},
- "package_type": {
+ "packageType": {
"type": ["string"],
"enum": ["MAVEN", "NPM", "CONAN", "NUGET", "PYPI", "COMPOSER", "GENERIC", "GOLANG", "DEBIAN"]
},
"tags": {
- "type": "object"
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "pageInfo": { "type": "object" },
+ "edges": { "type": "array" },
+ "nodes": { "type": "array" }
+ }
},
"project": {
"type": "object"
},
"pipelines": {
- "type": "object"
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "pageInfo": { "type": "object" },
+ "count": { "type": "integer" },
+ "edges": { "type": "array" },
+ "nodes": { "type": "array" }
+ }
},
"versions": {
- "type": "object"
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "pageInfo": { "type": "object" },
+ "edges": { "type": "array" },
+ "nodes": { "type": "array" }
+ }
+ },
+ "metadata": {
+ "anyOf": [
+ { "$ref": "./package_composer_metadata.json" },
+ { "type": "null" }
+ ]
}
}
}
diff --git a/spec/fixtures/api/schemas/group_group_links.json b/spec/fixtures/api/schemas/group_group_links.json
deleted file mode 100644
index f8b4e7f035b..00000000000
--- a/spec/fixtures/api/schemas/group_group_links.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "type": "array",
- "items": {
- "$ref": "entities/group_group_link.json"
- }
-}
diff --git a/spec/fixtures/api/schemas/group_link/group_group_link.json b/spec/fixtures/api/schemas/group_link/group_group_link.json
new file mode 100644
index 00000000000..bfca5c885e3
--- /dev/null
+++ b/spec/fixtures/api/schemas/group_link/group_group_link.json
@@ -0,0 +1,16 @@
+{
+ "type": "object",
+ "allOf": [
+ { "$ref": "group_link.json" },
+ {
+ "required": [
+ "can_update",
+ "can_remove"
+ ],
+ "properties": {
+ "can_update": { "type": "boolean" },
+ "can_remove": { "type": "boolean" }
+ }
+ }
+ ]
+}
diff --git a/spec/fixtures/api/schemas/group_link/group_group_links.json b/spec/fixtures/api/schemas/group_link/group_group_links.json
new file mode 100644
index 00000000000..2c0bf20f524
--- /dev/null
+++ b/spec/fixtures/api/schemas/group_link/group_group_links.json
@@ -0,0 +1,6 @@
+{
+ "type": "array",
+ "items": {
+ "$ref": "group_group_link.json"
+ }
+}
diff --git a/spec/fixtures/api/schemas/entities/group_group_link.json b/spec/fixtures/api/schemas/group_link/group_link.json
index bf94bbb3ce4..300790728a8 100644
--- a/spec/fixtures/api/schemas/entities/group_group_link.json
+++ b/spec/fixtures/api/schemas/group_link/group_link.json
@@ -4,8 +4,6 @@
"id",
"created_at",
"expires_at",
- "can_update",
- "can_remove",
"access_level",
"valid_roles"
],
@@ -13,15 +11,14 @@
"id": { "type": "integer" },
"created_at": { "type": "date-time" },
"expires_at": { "type": ["date-time", "null"] },
- "can_update": { "type": "boolean" },
- "can_remove": { "type": "boolean" },
"access_level": {
"type": "object",
"required": ["integer_value", "string_value"],
"properties": {
"integer_value": { "type": "integer" },
"string_value": { "type": "string" }
- }
+ },
+ "additionalProperties": false
},
"valid_roles": { "type": "object" },
"shared_with_group": {
@@ -34,7 +31,8 @@
"full_path": { "type": "string" },
"avatar_url": { "type": ["string", "null"] },
"web_url": { "type": "string" }
- }
+ },
+ "additionalProperties": false
}
}
}
diff --git a/spec/fixtures/api/schemas/group_link/project_group_link.json b/spec/fixtures/api/schemas/group_link/project_group_link.json
new file mode 100644
index 00000000000..bfca5c885e3
--- /dev/null
+++ b/spec/fixtures/api/schemas/group_link/project_group_link.json
@@ -0,0 +1,16 @@
+{
+ "type": "object",
+ "allOf": [
+ { "$ref": "group_link.json" },
+ {
+ "required": [
+ "can_update",
+ "can_remove"
+ ],
+ "properties": {
+ "can_update": { "type": "boolean" },
+ "can_remove": { "type": "boolean" }
+ }
+ }
+ ]
+}
diff --git a/spec/fixtures/api/schemas/group_link/project_group_links.json b/spec/fixtures/api/schemas/group_link/project_group_links.json
new file mode 100644
index 00000000000..fc024d67f36
--- /dev/null
+++ b/spec/fixtures/api/schemas/group_link/project_group_links.json
@@ -0,0 +1,6 @@
+{
+ "type": "array",
+ "items": {
+ "$ref": "project_group_link.json"
+ }
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/job.json b/spec/fixtures/api/schemas/public_api/v4/job.json
index c038ae0a664..b50479841a9 100644
--- a/spec/fixtures/api/schemas/public_api/v4/job.json
+++ b/spec/fixtures/api/schemas/public_api/v4/job.json
@@ -18,6 +18,7 @@
"web_url",
"artifacts",
"artifacts_expire_at",
+ "tag_list",
"runner"
],
"properties": {
@@ -53,6 +54,9 @@
]
},
"artifacts_expire_at": { "type": ["null", "string"] },
+ "tag_list": {
+ "type": "array"
+ },
"runner": {
"oneOf": [
{ "type": "null" },
diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb
index aff4b1aae23..100d17cc16e 100644
--- a/spec/fixtures/markdown.md.erb
+++ b/spec/fixtures/markdown.md.erb
@@ -170,6 +170,8 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- Ignores invalid: <%= User.reference_prefix %>fake_user
- Ignored in code: `<%= user.to_reference %>`
- Ignored in links: [Link to <%= user.to_reference %>](#user-link)
+- Ignored when backslash escaped: \<%= user.to_reference %>
+- Ignored when backslash escaped: \<%= group.to_reference %>
- Link to user by reference: [User](<%= user.to_reference %>)
#### IssueReferenceFilter
@@ -178,6 +180,7 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- Issue in another project: <%= xissue.to_reference(project) %>
- Ignored in code: `<%= issue.to_reference %>`
- Ignored in links: [Link to <%= issue.to_reference %>](#issue-link)
+- Ignored when backslash escaped: \<%= issue.to_reference %>
- Issue by URL: <%= urls.project_issue_url(issue.project, issue) %>
- Link to issue by reference: [Issue](<%= issue.to_reference %>)
- Link to issue by URL: [Issue](<%= urls.project_issue_url(issue.project, issue) %>)
@@ -188,6 +191,7 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- Merge request in another project: <%= xmerge_request.to_reference(project) %>
- Ignored in code: `<%= merge_request.to_reference %>`
- Ignored in links: [Link to <%= merge_request.to_reference %>](#merge-request-link)
+- Ignored when backslash escaped: \<%= merge_request.to_reference %>
- Merge request by URL: <%= urls.project_merge_request_url(merge_request.project, merge_request) %>
- Link to merge request by reference: [Merge request](<%= merge_request.to_reference %>)
- Link to merge request by URL: [Merge request](<%= urls.project_merge_request_url(merge_request.project, merge_request) %>)
@@ -198,6 +202,7 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- Snippet in another project: <%= xsnippet.to_reference(project) %>
- Ignored in code: `<%= snippet.to_reference %>`
- Ignored in links: [Link to <%= snippet.to_reference %>](#snippet-link)
+- Ignored when backslash escaped: \<%= snippet.to_reference %>
- Snippet by URL: <%= urls.project_snippet_url(snippet.project, snippet) %>
- Link to snippet by reference: [Snippet](<%= snippet.to_reference %>)
- Link to snippet by URL: [Snippet](<%= urls.project_snippet_url(snippet.project, snippet) %>)
@@ -229,6 +234,7 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- Label by name in quotes: <%= label.to_reference(format: :name) %>
- Ignored in code: `<%= simple_label.to_reference %>`
- Ignored in links: [Link to <%= simple_label.to_reference %>](#label-link)
+- Ignored when backslash escaped: \<%= simple_label.to_reference %>
- Link to label by reference: [Label](<%= label.to_reference %>)
#### MilestoneReferenceFilter
@@ -239,6 +245,7 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- Milestone in another project: <%= xmilestone.to_reference(project) %>
- Ignored in code: `<%= simple_milestone.to_reference %>`
- Ignored in links: [Link to <%= simple_milestone.to_reference %>](#milestone-link)
+- Ignored when backslash escaped: \<%= simple_milestone.to_reference %>
- Milestone by URL: <%= urls.milestone_url(milestone) %>
- Link to milestone by URL: [Milestone](<%= milestone.to_reference %>)
- Group milestone by name: <%= Milestone.reference_prefix %><%= group_milestone.name %>
@@ -250,6 +257,7 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- Alert in another project: <%= xalert.to_reference(project) %>
- Ignored in code: `<%= alert.to_reference %>`
- Ignored in links: [Link to <%= alert.to_reference %>](#alert-link)
+- Ignored when backslash escaped: \<%= alert.to_reference %>
- Alert by URL: <%= alert.details_url %>
- Link to alert by reference: [Alert](<%= alert.to_reference %>)
- Link to alert by URL: [Alert](<%= alert.details_url %>)
@@ -350,3 +358,17 @@ For details see the [Mermaid official page][mermaid].
[mermaid]: https://mermaidjs.github.io/ "Mermaid website"
+### PLantUML
+
+```plantuml
+Bob -> Sara : Hello
+```
+
+### Kroki
+
+```nomnoml
+[Pirate|eyeCount: Int|raid();pillage()|
+ [beard]--[parrot]
+ [beard]-:>[foul mouth]
+]
+```
diff --git a/spec/fixtures/packages/composer/package.json b/spec/fixtures/packages/composer/package.json
new file mode 100644
index 00000000000..0967ef424bc
--- /dev/null
+++ b/spec/fixtures/packages/composer/package.json
@@ -0,0 +1 @@
+{}
diff --git a/spec/fixtures/packages/debian/distribution/Packages b/spec/fixtures/packages/debian/distribution/Packages
new file mode 100644
index 00000000000..d2d8af553d7
--- /dev/null
+++ b/spec/fixtures/packages/debian/distribution/Packages
@@ -0,0 +1,2 @@
+Package: example-package
+Description: This is an incomplete Packages file
diff --git a/spec/fixtures/packages/debian/distribution/Release b/spec/fixtures/packages/debian/distribution/Release
new file mode 100644
index 00000000000..a2d62c45645
--- /dev/null
+++ b/spec/fixtures/packages/debian/distribution/Release
@@ -0,0 +1 @@
+Codename: fixture-distribution
diff --git a/spec/fixtures/packages/rubygems/package-0.0.1.gem b/spec/fixtures/packages/rubygems/package-0.0.1.gem
new file mode 100644
index 00000000000..2143ef408ac
--- /dev/null
+++ b/spec/fixtures/packages/rubygems/package-0.0.1.gem
Binary files differ
diff --git a/spec/fixtures/packages/rubygems/package.gemspec b/spec/fixtures/packages/rubygems/package.gemspec
new file mode 100644
index 00000000000..bb87c47f5dc
--- /dev/null
+++ b/spec/fixtures/packages/rubygems/package.gemspec
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+Gem::Specification.new do |s|
+ s.name = %q{package}
+ s.authors = ["Tanuki Steve"]
+ s.version = "0.0.1"
+ s.date = %q{2011-09-29}
+ s.summary = %q{package is the best}
+ s.files = [
+ "lib/package.rb"
+ ]
+ s.required_ruby_version = '>= 2.7.0'
+ s.rubygems_version = '>= 1.8.11'
+ s.require_paths = ["lib"]
+end
diff --git a/spec/fixtures/pipeline_artifacts/code_quality_mr_diff.json b/spec/fixtures/pipeline_artifacts/code_quality_mr_diff.json
new file mode 100644
index 00000000000..c3ee2bc4cac
--- /dev/null
+++ b/spec/fixtures/pipeline_artifacts/code_quality_mr_diff.json
@@ -0,0 +1,23 @@
+{
+ "files": {
+ "file_a.rb": [
+ {
+ "line": 10,
+ "description": "Avoid parameter lists longer than 5 parameters. [12/5]",
+ "severity": "major"
+ },
+ {
+ "line": 10,
+ "description": "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.",
+ "severity": "minor"
+ }
+ ],
+ "file_b.rb": [
+ {
+ "line": 10,
+ "description": "This cop checks for methods with too many parameters.\nThe maximum number of parameters is configurable.\nKeyword arguments can optionally be excluded from the total count.",
+ "severity": "minor"
+ }
+ ]
+ }
+}
diff --git a/spec/fixtures/security_reports/master/gl-sast-report.json b/spec/fixtures/security_reports/master/gl-sast-report.json
new file mode 100644
index 00000000000..98bb15e349f
--- /dev/null
+++ b/spec/fixtures/security_reports/master/gl-sast-report.json
@@ -0,0 +1,983 @@
+{
+ "version": "1.2",
+ "vulnerabilities": [
+ {
+ "category": "sast",
+ "message": "Probable insecure usage of temp file/directory.",
+ "cve": "python/hardcoded/hardcoded-tmp.py:52865813c884a507be1f152d654245af34aba8a391626d01f1ab6d3f52ec8779:B108",
+ "severity": "Medium",
+ "confidence": "Medium",
+ "scanner": {
+ "id": "bandit",
+ "name": "Bandit"
+ },
+ "location": {
+ "file": "python/hardcoded/hardcoded-tmp.py",
+ "start_line": 1,
+ "end_line": 1
+ },
+ "identifiers": [
+ {
+ "type": "bandit_test_id",
+ "name": "Bandit Test ID B108",
+ "value": "B108",
+ "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html"
+ }
+ ],
+ "priority": "Medium",
+ "file": "python/hardcoded/hardcoded-tmp.py",
+ "line": 1,
+ "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html",
+ "tool": "bandit"
+ },
+ {
+ "category": "sast",
+ "name": "Predictable pseudorandom number generator",
+ "message": "Predictable pseudorandom number generator",
+ "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:47:PREDICTABLE_RANDOM",
+ "severity": "Medium",
+ "confidence": "Medium",
+ "scanner": {
+ "id": "find_sec_bugs",
+ "name": "Find Security Bugs"
+ },
+ "location": {
+ "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy",
+ "start_line": 47,
+ "end_line": 47,
+ "class": "com.gitlab.security_products.tests.App",
+ "method": "generateSecretToken2"
+ },
+ "identifiers": [
+ {
+ "type": "find_sec_bugs_type",
+ "name": "Find Security Bugs-PREDICTABLE_RANDOM",
+ "value": "PREDICTABLE_RANDOM",
+ "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM"
+ }
+ ],
+ "priority": "Medium",
+ "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy",
+ "line": 47,
+ "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM",
+ "tool": "find_sec_bugs"
+ },
+ {
+ "category": "sast",
+ "name": "Predictable pseudorandom number generator",
+ "message": "Predictable pseudorandom number generator",
+ "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:41:PREDICTABLE_RANDOM",
+ "severity": "Medium",
+ "confidence": "Medium",
+ "scanner": {
+ "id": "find_sec_bugs",
+ "name": "Find Security Bugs"
+ },
+ "location": {
+ "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy",
+ "start_line": 41,
+ "end_line": 41,
+ "class": "com.gitlab.security_products.tests.App",
+ "method": "generateSecretToken1"
+ },
+ "identifiers": [
+ {
+ "type": "find_sec_bugs_type",
+ "name": "Find Security Bugs-PREDICTABLE_RANDOM",
+ "value": "PREDICTABLE_RANDOM",
+ "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM"
+ }
+ ],
+ "priority": "Medium",
+ "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy",
+ "line": 41,
+ "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM",
+ "tool": "find_sec_bugs"
+ },
+ {
+ "category": "sast",
+ "message": "Use of insecure MD2, MD4, or MD5 hash function.",
+ "cve": "python/imports/imports-aliases.py:cb203b465dffb0cb3a8e8bd8910b84b93b0a5995a938e4b903dbb0cd6ffa1254:B303",
+ "severity": "Medium",
+ "confidence": "High",
+ "scanner": {
+ "id": "bandit",
+ "name": "Bandit"
+ },
+ "location": {
+ "file": "python/imports/imports-aliases.py",
+ "start_line": 11,
+ "end_line": 11
+ },
+ "identifiers": [
+ {
+ "type": "bandit_test_id",
+ "name": "Bandit Test ID B303",
+ "value": "B303"
+ }
+ ],
+ "priority": "Medium",
+ "file": "python/imports/imports-aliases.py",
+ "line": 11,
+ "tool": "bandit"
+ },
+ {
+ "category": "sast",
+ "message": "Use of insecure MD2, MD4, or MD5 hash function.",
+ "cve": "python/imports/imports-aliases.py:a7173c43ae66bd07466632d819d450e0071e02dbf782763640d1092981f9631b:B303",
+ "severity": "Medium",
+ "confidence": "High",
+ "scanner": {
+ "id": "bandit",
+ "name": "Bandit"
+ },
+ "location": {
+ "file": "python/imports/imports-aliases.py",
+ "start_line": 12,
+ "end_line": 12
+ },
+ "identifiers": [
+ {
+ "type": "bandit_test_id",
+ "name": "Bandit Test ID B303",
+ "value": "B303"
+ }
+ ],
+ "priority": "Medium",
+ "file": "python/imports/imports-aliases.py",
+ "line": 12,
+ "tool": "bandit"
+ },
+ {
+ "category": "sast",
+ "message": "Use of insecure MD2, MD4, or MD5 hash function.",
+ "cve": "python/imports/imports-aliases.py:017017b77deb0b8369b6065947833eeea752a92ec8a700db590fece3e934cf0d:B303",
+ "severity": "Medium",
+ "confidence": "High",
+ "scanner": {
+ "id": "bandit",
+ "name": "Bandit"
+ },
+ "location": {
+ "file": "python/imports/imports-aliases.py",
+ "start_line": 13,
+ "end_line": 13
+ },
+ "identifiers": [
+ {
+ "type": "bandit_test_id",
+ "name": "Bandit Test ID B303",
+ "value": "B303"
+ }
+ ],
+ "priority": "Medium",
+ "file": "python/imports/imports-aliases.py",
+ "line": 13,
+ "tool": "bandit"
+ },
+ {
+ "category": "sast",
+ "message": "Use of insecure MD2, MD4, or MD5 hash function.",
+ "cve": "python/imports/imports-aliases.py:45fc8c53aea7b84f06bc4e590cc667678d6073c4c8a1d471177ca2146fb22db2:B303",
+ "severity": "Medium",
+ "confidence": "High",
+ "scanner": {
+ "id": "bandit",
+ "name": "Bandit"
+ },
+ "location": {
+ "file": "python/imports/imports-aliases.py",
+ "start_line": 14,
+ "end_line": 14
+ },
+ "identifiers": [
+ {
+ "type": "bandit_test_id",
+ "name": "Bandit Test ID B303",
+ "value": "B303"
+ }
+ ],
+ "priority": "Medium",
+ "file": "python/imports/imports-aliases.py",
+ "line": 14,
+ "tool": "bandit"
+ },
+ {
+ "category": "sast",
+ "message": "Pickle library appears to be in use, possible security issue.",
+ "cve": "python/imports/imports-aliases.py:5f200d47291e7bbd8352db23019b85453ca048dd98ea0c291260fa7d009963a4:B301",
+ "severity": "Medium",
+ "confidence": "High",
+ "scanner": {
+ "id": "bandit",
+ "name": "Bandit"
+ },
+ "location": {
+ "file": "python/imports/imports-aliases.py",
+ "start_line": 15,
+ "end_line": 15
+ },
+ "identifiers": [
+ {
+ "type": "bandit_test_id",
+ "name": "Bandit Test ID B301",
+ "value": "B301"
+ }
+ ],
+ "priority": "Medium",
+ "file": "python/imports/imports-aliases.py",
+ "line": 15,
+ "tool": "bandit"
+ },
+ {
+ "category": "sast",
+ "name": "ECB mode is insecure",
+ "message": "ECB mode is insecure",
+ "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:29:ECB_MODE",
+ "severity": "Medium",
+ "confidence": "High",
+ "scanner": {
+ "id": "find_sec_bugs",
+ "name": "Find Security Bugs"
+ },
+ "location": {
+ "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy",
+ "start_line": 29,
+ "end_line": 29,
+ "class": "com.gitlab.security_products.tests.App",
+ "method": "insecureCypher"
+ },
+ "identifiers": [
+ {
+ "type": "find_sec_bugs_type",
+ "name": "Find Security Bugs-ECB_MODE",
+ "value": "ECB_MODE",
+ "url": "https://find-sec-bugs.github.io/bugs.htm#ECB_MODE"
+ }
+ ],
+ "priority": "Medium",
+ "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy",
+ "line": 29,
+ "url": "https://find-sec-bugs.github.io/bugs.htm#ECB_MODE",
+ "tool": "find_sec_bugs"
+ },
+ {
+ "category": "sast",
+ "name": "Cipher with no integrity",
+ "message": "Cipher with no integrity",
+ "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:29:CIPHER_INTEGRITY",
+ "severity": "Medium",
+ "confidence": "High",
+ "scanner": {
+ "id": "find_sec_bugs",
+ "name": "Find Security Bugs"
+ },
+ "location": {
+ "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy",
+ "start_line": 29,
+ "end_line": 29,
+ "class": "com.gitlab.security_products.tests.App",
+ "method": "insecureCypher"
+ },
+ "identifiers": [
+ {
+ "type": "find_sec_bugs_type",
+ "name": "Find Security Bugs-CIPHER_INTEGRITY",
+ "value": "CIPHER_INTEGRITY",
+ "url": "https://find-sec-bugs.github.io/bugs.htm#CIPHER_INTEGRITY"
+ }
+ ],
+ "priority": "Medium",
+ "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy",
+ "line": 29,
+ "url": "https://find-sec-bugs.github.io/bugs.htm#CIPHER_INTEGRITY",
+ "tool": "find_sec_bugs"
+ },
+ {
+ "category": "sast",
+ "message": "Probable insecure usage of temp file/directory.",
+ "cve": "python/hardcoded/hardcoded-tmp.py:63dd4d626855555b816985d82c4614a790462a0a3ada89dc58eb97f9c50f3077:B108",
+ "severity": "Medium",
+ "confidence": "Medium",
+ "scanner": {
+ "id": "bandit",
+ "name": "Bandit"
+ },
+ "location": {
+ "file": "python/hardcoded/hardcoded-tmp.py",
+ "start_line": 14,
+ "end_line": 14
+ },
+ "identifiers": [
+ {
+ "type": "bandit_test_id",
+ "name": "Bandit Test ID B108",
+ "value": "B108",
+ "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html"
+ }
+ ],
+ "priority": "Medium",
+ "file": "python/hardcoded/hardcoded-tmp.py",
+ "line": 14,
+ "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html",
+ "tool": "bandit"
+ },
+ {
+ "category": "sast",
+ "message": "Probable insecure usage of temp file/directory.",
+ "cve": "python/hardcoded/hardcoded-tmp.py:4ad6d4c40a8c263fc265f3384724014e0a4f8dd6200af83e51ff120420038031:B108",
+ "severity": "Medium",
+ "confidence": "Medium",
+ "scanner": {
+ "id": "bandit",
+ "name": "Bandit"
+ },
+ "location": {
+ "file": "python/hardcoded/hardcoded-tmp.py",
+ "start_line": 10,
+ "end_line": 10
+ },
+ "identifiers": [
+ {
+ "type": "bandit_test_id",
+ "name": "Bandit Test ID B108",
+ "value": "B108",
+ "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html"
+ }
+ ],
+ "priority": "Medium",
+ "file": "python/hardcoded/hardcoded-tmp.py",
+ "line": 10,
+ "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html",
+ "tool": "bandit"
+ },
+ {
+ "category": "sast",
+ "message": "Consider possible security implications associated with Popen module.",
+ "cve": "python/imports/imports-aliases.py:2c3e1fa1e54c3c6646e8bcfaee2518153c6799b77587ff8d9a7b0631f6d34785:B404",
+ "severity": "Low",
+ "confidence": "High",
+ "scanner": {
+ "id": "bandit",
+ "name": "Bandit"
+ },
+ "location": {
+ "file": "python/imports/imports-aliases.py",
+ "start_line": 1,
+ "end_line": 1
+ },
+ "identifiers": [
+ {
+ "type": "bandit_test_id",
+ "name": "Bandit Test ID B404",
+ "value": "B404"
+ }
+ ],
+ "priority": "Low",
+ "file": "python/imports/imports-aliases.py",
+ "line": 1,
+ "tool": "bandit"
+ },
+ {
+ "category": "sast",
+ "message": "Consider possible security implications associated with pickle module.",
+ "cve": "python/imports/imports.py:af58d07f6ad519ef5287fcae65bf1a6999448a1a3a8bc1ac2a11daa80d0b96bf:B403",
+ "severity": "Low",
+ "confidence": "High",
+ "scanner": {
+ "id": "bandit",
+ "name": "Bandit"
+ },
+ "location": {
+ "file": "python/imports/imports.py",
+ "start_line": 2,
+ "end_line": 2
+ },
+ "identifiers": [
+ {
+ "type": "bandit_test_id",
+ "name": "Bandit Test ID B403",
+ "value": "B403"
+ }
+ ],
+ "priority": "Low",
+ "file": "python/imports/imports.py",
+ "line": 2,
+ "tool": "bandit"
+ },
+ {
+ "category": "sast",
+ "message": "Consider possible security implications associated with subprocess module.",
+ "cve": "python/imports/imports.py:8de9bc98029d212db530785a5f6780cfa663548746ff228ab8fa96c5bb82f089:B404",
+ "severity": "Low",
+ "confidence": "High",
+ "scanner": {
+ "id": "bandit",
+ "name": "Bandit"
+ },
+ "location": {
+ "file": "python/imports/imports.py",
+ "start_line": 4,
+ "end_line": 4
+ },
+ "identifiers": [
+ {
+ "type": "bandit_test_id",
+ "name": "Bandit Test ID B404",
+ "value": "B404"
+ }
+ ],
+ "priority": "Low",
+ "file": "python/imports/imports.py",
+ "line": 4,
+ "tool": "bandit"
+ },
+ {
+ "category": "sast",
+ "message": "Possible hardcoded password: 'blerg'",
+ "cve": "python/hardcoded/hardcoded-passwords.py:97c30f1d76d2a88913e3ce9ae74087874d740f87de8af697a9c455f01119f633:B106",
+ "severity": "Low",
+ "confidence": "Medium",
+ "scanner": {
+ "id": "bandit",
+ "name": "Bandit"
+ },
+ "location": {
+ "file": "python/hardcoded/hardcoded-passwords.py",
+ "start_line": 22,
+ "end_line": 22
+ },
+ "identifiers": [
+ {
+ "type": "bandit_test_id",
+ "name": "Bandit Test ID B106",
+ "value": "B106",
+ "url": "https://docs.openstack.org/bandit/latest/plugins/b106_hardcoded_password_funcarg.html"
+ }
+ ],
+ "priority": "Low",
+ "file": "python/hardcoded/hardcoded-passwords.py",
+ "line": 22,
+ "url": "https://docs.openstack.org/bandit/latest/plugins/b106_hardcoded_password_funcarg.html",
+ "tool": "bandit"
+ },
+ {
+ "category": "sast",
+ "message": "Possible hardcoded password: 'root'",
+ "cve": "python/hardcoded/hardcoded-passwords.py:7431c73a0bc16d94ece2a2e75ef38f302574d42c37ac0c3c38ad0b3bf8a59f10:B105",
+ "severity": "Low",
+ "confidence": "Medium",
+ "scanner": {
+ "id": "bandit",
+ "name": "Bandit"
+ },
+ "location": {
+ "file": "python/hardcoded/hardcoded-passwords.py",
+ "start_line": 5,
+ "end_line": 5
+ },
+ "identifiers": [
+ {
+ "type": "bandit_test_id",
+ "name": "Bandit Test ID B105",
+ "value": "B105",
+ "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html"
+ }
+ ],
+ "priority": "Low",
+ "file": "python/hardcoded/hardcoded-passwords.py",
+ "line": 5,
+ "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html",
+ "tool": "bandit"
+ },
+ {
+ "category": "sast",
+ "message": "Possible hardcoded password: ''",
+ "cve": "python/hardcoded/hardcoded-passwords.py:d2d1857c27caedd49c57bfbcdc23afcc92bd66a22701fcdc632869aab4ca73ee:B105",
+ "severity": "Low",
+ "confidence": "Medium",
+ "scanner": {
+ "id": "bandit",
+ "name": "Bandit"
+ },
+ "location": {
+ "file": "python/hardcoded/hardcoded-passwords.py",
+ "start_line": 9,
+ "end_line": 9
+ },
+ "identifiers": [
+ {
+ "type": "bandit_test_id",
+ "name": "Bandit Test ID B105",
+ "value": "B105",
+ "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html"
+ }
+ ],
+ "priority": "Low",
+ "file": "python/hardcoded/hardcoded-passwords.py",
+ "line": 9,
+ "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html",
+ "tool": "bandit"
+ },
+ {
+ "category": "sast",
+ "message": "Possible hardcoded password: 'ajklawejrkl42348swfgkg'",
+ "cve": "python/hardcoded/hardcoded-passwords.py:fb3866215a61393a5c9c32a3b60e2058171a23219c353f722cbd3567acab21d2:B105",
+ "severity": "Low",
+ "confidence": "Medium",
+ "scanner": {
+ "id": "bandit",
+ "name": "Bandit"
+ },
+ "location": {
+ "file": "python/hardcoded/hardcoded-passwords.py",
+ "start_line": 13,
+ "end_line": 13
+ },
+ "identifiers": [
+ {
+ "type": "bandit_test_id",
+ "name": "Bandit Test ID B105",
+ "value": "B105",
+ "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html"
+ }
+ ],
+ "priority": "Low",
+ "file": "python/hardcoded/hardcoded-passwords.py",
+ "line": 13,
+ "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html",
+ "tool": "bandit"
+ },
+ {
+ "category": "sast",
+ "message": "Possible hardcoded password: 'blerg'",
+ "cve": "python/hardcoded/hardcoded-passwords.py:63c62a8b7e1e5224439bd26b28030585ac48741e28ca64561a6071080c560a5f:B105",
+ "severity": "Low",
+ "confidence": "Medium",
+ "scanner": {
+ "id": "bandit",
+ "name": "Bandit"
+ },
+ "location": {
+ "file": "python/hardcoded/hardcoded-passwords.py",
+ "start_line": 23,
+ "end_line": 23
+ },
+ "identifiers": [
+ {
+ "type": "bandit_test_id",
+ "name": "Bandit Test ID B105",
+ "value": "B105",
+ "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html"
+ }
+ ],
+ "priority": "Low",
+ "file": "python/hardcoded/hardcoded-passwords.py",
+ "line": 23,
+ "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html",
+ "tool": "bandit"
+ },
+ {
+ "category": "sast",
+ "message": "Possible hardcoded password: 'blerg'",
+ "cve": "python/hardcoded/hardcoded-passwords.py:4311b06d08df8fa58229b341c531da8e1a31ec4520597bdff920cd5c098d86f9:B105",
+ "severity": "Low",
+ "confidence": "Medium",
+ "scanner": {
+ "id": "bandit",
+ "name": "Bandit"
+ },
+ "location": {
+ "file": "python/hardcoded/hardcoded-passwords.py",
+ "start_line": 24,
+ "end_line": 24
+ },
+ "identifiers": [
+ {
+ "type": "bandit_test_id",
+ "name": "Bandit Test ID B105",
+ "value": "B105",
+ "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html"
+ }
+ ],
+ "priority": "Low",
+ "file": "python/hardcoded/hardcoded-passwords.py",
+ "line": 24,
+ "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html",
+ "tool": "bandit"
+ },
+ {
+ "category": "sast",
+ "message": "Consider possible security implications associated with subprocess module.",
+ "cve": "python/imports/imports-function.py:5858400c2f39047787702de44d03361ef8d954c9d14bd54ee1c2bef9e6a7df93:B404",
+ "severity": "Low",
+ "confidence": "High",
+ "scanner": {
+ "id": "bandit",
+ "name": "Bandit"
+ },
+ "location": {
+ "file": "python/imports/imports-function.py",
+ "start_line": 4,
+ "end_line": 4
+ },
+ "identifiers": [
+ {
+ "type": "bandit_test_id",
+ "name": "Bandit Test ID B404",
+ "value": "B404"
+ }
+ ],
+ "priority": "Low",
+ "file": "python/imports/imports-function.py",
+ "line": 4,
+ "tool": "bandit"
+ },
+ {
+ "category": "sast",
+ "message": "Consider possible security implications associated with pickle module.",
+ "cve": "python/imports/imports-function.py:dbda3cf4190279d30e0aad7dd137eca11272b0b225e8af4e8bf39682da67d956:B403",
+ "severity": "Low",
+ "confidence": "High",
+ "scanner": {
+ "id": "bandit",
+ "name": "Bandit"
+ },
+ "location": {
+ "file": "python/imports/imports-function.py",
+ "start_line": 2,
+ "end_line": 2
+ },
+ "identifiers": [
+ {
+ "type": "bandit_test_id",
+ "name": "Bandit Test ID B403",
+ "value": "B403"
+ }
+ ],
+ "priority": "Low",
+ "file": "python/imports/imports-function.py",
+ "line": 2,
+ "tool": "bandit"
+ },
+ {
+ "category": "sast",
+ "message": "Consider possible security implications associated with Popen module.",
+ "cve": "python/imports/imports-from.py:eb8a0db9cd1a8c1ab39a77e6025021b1261cc2a0b026b2f4a11fca4e0636d8dd:B404",
+ "severity": "Low",
+ "confidence": "High",
+ "scanner": {
+ "id": "bandit",
+ "name": "Bandit"
+ },
+ "location": {
+ "file": "python/imports/imports-from.py",
+ "start_line": 7,
+ "end_line": 7
+ },
+ "identifiers": [
+ {
+ "type": "bandit_test_id",
+ "name": "Bandit Test ID B404",
+ "value": "B404"
+ }
+ ],
+ "priority": "Low",
+ "file": "python/imports/imports-from.py",
+ "line": 7,
+ "tool": "bandit"
+ },
+ {
+ "category": "sast",
+ "message": "subprocess call with shell=True seems safe, but may be changed in the future, consider rewriting without shell",
+ "cve": "python/imports/imports-aliases.py:f99f9721e27537fbcb6699a4cf39c6740d6234d2c6f06cfc2d9ea977313c483d:B602",
+ "severity": "Low",
+ "confidence": "High",
+ "scanner": {
+ "id": "bandit",
+ "name": "Bandit"
+ },
+ "location": {
+ "file": "python/imports/imports-aliases.py",
+ "start_line": 9,
+ "end_line": 9
+ },
+ "identifiers": [
+ {
+ "type": "bandit_test_id",
+ "name": "Bandit Test ID B602",
+ "value": "B602",
+ "url": "https://docs.openstack.org/bandit/latest/plugins/b602_subprocess_popen_with_shell_equals_true.html"
+ }
+ ],
+ "priority": "Low",
+ "file": "python/imports/imports-aliases.py",
+ "line": 9,
+ "url": "https://docs.openstack.org/bandit/latest/plugins/b602_subprocess_popen_with_shell_equals_true.html",
+ "tool": "bandit"
+ },
+ {
+ "category": "sast",
+ "message": "Consider possible security implications associated with subprocess module.",
+ "cve": "python/imports/imports-from.py:332a12ab1146698f614a905ce6a6a5401497a12281aef200e80522711c69dcf4:B404",
+ "severity": "Low",
+ "confidence": "High",
+ "scanner": {
+ "id": "bandit",
+ "name": "Bandit"
+ },
+ "location": {
+ "file": "python/imports/imports-from.py",
+ "start_line": 6,
+ "end_line": 6
+ },
+ "identifiers": [
+ {
+ "type": "bandit_test_id",
+ "name": "Bandit Test ID B404",
+ "value": "B404"
+ }
+ ],
+ "priority": "Low",
+ "file": "python/imports/imports-from.py",
+ "line": 6,
+ "tool": "bandit"
+ },
+ {
+ "category": "sast",
+ "message": "Consider possible security implications associated with Popen module.",
+ "cve": "python/imports/imports-from.py:0a48de4a3d5348853a03666cb574697e3982998355e7a095a798bd02a5947276:B404",
+ "severity": "Low",
+ "confidence": "High",
+ "scanner": {
+ "id": "bandit",
+ "name": "Bandit"
+ },
+ "location": {
+ "file": "python/imports/imports-from.py",
+ "start_line": 1,
+ "end_line": 2
+ },
+ "identifiers": [
+ {
+ "type": "bandit_test_id",
+ "name": "Bandit Test ID B404",
+ "value": "B404"
+ }
+ ],
+ "priority": "Low",
+ "file": "python/imports/imports-from.py",
+ "line": 1,
+ "tool": "bandit"
+ },
+ {
+ "category": "sast",
+ "message": "Consider possible security implications associated with pickle module.",
+ "cve": "python/imports/imports-aliases.py:51b71661dff994bde3529639a727a678c8f5c4c96f00d300913f6d5be1bbdf26:B403",
+ "severity": "Low",
+ "confidence": "High",
+ "scanner": {
+ "id": "bandit",
+ "name": "Bandit"
+ },
+ "location": {
+ "file": "python/imports/imports-aliases.py",
+ "start_line": 7,
+ "end_line": 8
+ },
+ "identifiers": [
+ {
+ "type": "bandit_test_id",
+ "name": "Bandit Test ID B403",
+ "value": "B403"
+ }
+ ],
+ "priority": "Low",
+ "file": "python/imports/imports-aliases.py",
+ "line": 7,
+ "tool": "bandit"
+ },
+ {
+ "category": "sast",
+ "message": "Consider possible security implications associated with loads module.",
+ "cve": "python/imports/imports-aliases.py:6ff02aeb3149c01ab68484d794a94f58d5d3e3bb0d58557ef4153644ea68ea54:B403",
+ "severity": "Low",
+ "confidence": "High",
+ "scanner": {
+ "id": "bandit",
+ "name": "Bandit"
+ },
+ "location": {
+ "file": "python/imports/imports-aliases.py",
+ "start_line": 6,
+ "end_line": 6
+ },
+ "identifiers": [
+ {
+ "type": "bandit_test_id",
+ "name": "Bandit Test ID B403",
+ "value": "B403"
+ }
+ ],
+ "priority": "Low",
+ "file": "python/imports/imports-aliases.py",
+ "line": 6,
+ "tool": "bandit"
+ },
+ {
+ "category": "sast",
+ "message": "Statically-sized arrays can be improperly restricted, leading to potential overflows or other issues (CWE-119!/CWE-120)",
+ "cve": "c/subdir/utils.c:b466873101951fe96e1332f6728eb7010acbbd5dfc3b65d7d53571d091a06d9e:CWE-119!/CWE-120",
+ "confidence": "Low",
+ "solution": "Perform bounds checking, use functions that limit length, or ensure that the size is larger than the maximum possible length",
+ "scanner": {
+ "id": "flawfinder",
+ "name": "Flawfinder"
+ },
+ "location": {
+ "file": "c/subdir/utils.c",
+ "start_line": 4
+ },
+ "identifiers": [
+ {
+ "type": "flawfinder_func_name",
+ "name": "Flawfinder - char",
+ "value": "char"
+ },
+ {
+ "type": "cwe",
+ "name": "CWE-119",
+ "value": "119",
+ "url": "https://cwe.mitre.org/data/definitions/119.html"
+ },
+ {
+ "type": "cwe",
+ "name": "CWE-120",
+ "value": "120",
+ "url": "https://cwe.mitre.org/data/definitions/120.html"
+ }
+ ],
+ "file": "c/subdir/utils.c",
+ "line": 4,
+ "url": "https://cwe.mitre.org/data/definitions/119.html",
+ "tool": "flawfinder"
+ },
+ {
+ "category": "sast",
+ "message": "Check when opening files - can an attacker redirect it (via symlinks), force the opening of special file type (e.g., device files), move things around to create a race condition, control its ancestors, or change its contents? (CWE-362)",
+ "cve": "c/subdir/utils.c:bab681140fcc8fc3085b6bba74081b44ea145c1c98b5e70cf19ace2417d30770:CWE-362",
+ "confidence": "Low",
+ "scanner": {
+ "id": "flawfinder",
+ "name": "Flawfinder"
+ },
+ "location": {
+ "file": "c/subdir/utils.c",
+ "start_line": 8
+ },
+ "identifiers": [
+ {
+ "type": "flawfinder_func_name",
+ "name": "Flawfinder - fopen",
+ "value": "fopen"
+ },
+ {
+ "type": "cwe",
+ "name": "CWE-362",
+ "value": "362",
+ "url": "https://cwe.mitre.org/data/definitions/362.html"
+ }
+ ],
+ "file": "c/subdir/utils.c",
+ "line": 8,
+ "url": "https://cwe.mitre.org/data/definitions/362.html",
+ "tool": "flawfinder"
+ },
+ {
+ "category": "sast",
+ "message": "Statically-sized arrays can be improperly restricted, leading to potential overflows or other issues (CWE-119!/CWE-120)",
+ "cve": "cplusplus/src/hello.cpp:c8c6dd0afdae6814194cf0930b719f757ab7b379cf8f261e7f4f9f2f323a818a:CWE-119!/CWE-120",
+ "confidence": "Low",
+ "solution": "Perform bounds checking, use functions that limit length, or ensure that the size is larger than the maximum possible length",
+ "scanner": {
+ "id": "flawfinder",
+ "name": "Flawfinder"
+ },
+ "location": {
+ "file": "cplusplus/src/hello.cpp",
+ "start_line": 6
+ },
+ "identifiers": [
+ {
+ "type": "flawfinder_func_name",
+ "name": "Flawfinder - char",
+ "value": "char"
+ },
+ {
+ "type": "cwe",
+ "name": "CWE-119",
+ "value": "119",
+ "url": "https://cwe.mitre.org/data/definitions/119.html"
+ },
+ {
+ "type": "cwe",
+ "name": "CWE-120",
+ "value": "120",
+ "url": "https://cwe.mitre.org/data/definitions/120.html"
+ }
+ ],
+ "file": "cplusplus/src/hello.cpp",
+ "line": 6,
+ "url": "https://cwe.mitre.org/data/definitions/119.html",
+ "tool": "flawfinder"
+ },
+ {
+ "category": "sast",
+ "message": "Does not check for buffer overflows when copying to destination [MS-banned] (CWE-120)",
+ "cve": "cplusplus/src/hello.cpp:331c04062c4fe0c7c486f66f59e82ad146ab33cdd76ae757ca41f392d568cbd0:CWE-120",
+ "confidence": "Low",
+ "solution": "Consider using snprintf, strcpy_s, or strlcpy (warning: strncpy easily misused)",
+ "scanner": {
+ "id": "flawfinder",
+ "name": "Flawfinder"
+ },
+ "location": {
+ "file": "cplusplus/src/hello.cpp",
+ "start_line": 7
+ },
+ "identifiers": [
+ {
+ "type": "flawfinder_func_name",
+ "name": "Flawfinder - strcpy",
+ "value": "strcpy"
+ },
+ {
+ "type": "cwe",
+ "name": "CWE-120",
+ "value": "120",
+ "url": "https://cwe.mitre.org/data/definitions/120.html"
+ }
+ ],
+ "file": "cplusplus/src/hello.cpp",
+ "line": 7,
+ "url": "https://cwe.mitre.org/data/definitions/120.html",
+ "tool": "flawfinder"
+ }
+ ],
+ "remediations": [],
+ "scan": {
+ "scanner": {
+ "id": "gosec",
+ "name": "Gosec",
+ "url": "https://github.com/securego/gosec",
+ "vendor": {
+ "name": "GitLab"
+ },
+ "version": "2.3.0"
+ },
+ "type": "sast",
+ "status": "success",
+ "start_time": "placeholder-value",
+ "end_time": "placeholder-value"
+ }
+}
diff --git a/spec/fixtures/security_reports/master/gl-secret-detection-report.json b/spec/fixtures/security_reports/master/gl-secret-detection-report.json
new file mode 100644
index 00000000000..f0250ec9145
--- /dev/null
+++ b/spec/fixtures/security_reports/master/gl-secret-detection-report.json
@@ -0,0 +1,33 @@
+{
+ "version": "3.0",
+ "vulnerabilities": [
+ {
+ "id": "27d2322d519c94f803ffed1cf6d14e455df97e5a0668e229eb853fdb0d277d2c",
+ "category": "secret_detection",
+ "name": "AWS API key",
+ "message": "AWS API key",
+ "description": "Historic AWS secret has been found in commit 0830d9e4c0b43c0533cde798841b499e9df0653a.",
+ "cve": "aws-key.py:e275768c071cf6a6ea70a70b40f27c98debfe26bfe623c1539ec21c4478c6fca:AWS",
+ "severity": "Critical",
+ "confidence": "Unknown",
+ "scanner": {
+ "id": "gitleaks",
+ "name": "Gitleaks"
+ },
+ "location": {
+ "file": "aws-key.py",
+ "dependency": {
+ "package": {}
+ }
+ },
+ "identifiers": [
+ {
+ "type": "gitleaks_rule_id",
+ "name": "Gitleaks rule ID AWS",
+ "value": "AWS"
+ }
+ ]
+ }
+ ],
+ "remediations": []
+}
diff --git a/spec/fixtures/whats_new/invalid.yml b/spec/fixtures/whats_new/invalid.yml
index 0e588efaf8f..a3342be0f24 100644
--- a/spec/fixtures/whats_new/invalid.yml
+++ b/spec/fixtures/whats_new/invalid.yml
@@ -13,7 +13,7 @@
stage: Release
self-managed: true
gitlab-com: true
- packages: [Starter]
+ packages: [Free]
url: https://docs.gitlab.com/ee/ci/examples/authenticating-with-hashicorp-vault/index.html
image_url: https://about.gitlab.com/images/12_10/jwt-vault-1.png
published_at: 2020-04-22
diff --git a/spec/fixtures/whats_new/valid.yml b/spec/fixtures/whats_new/valid.yml
index cbe9d666357..ec465f47989 100644
--- a/spec/fixtures/whats_new/valid.yml
+++ b/spec/fixtures/whats_new/valid.yml
@@ -13,7 +13,7 @@
stage: Release
self-managed: true
gitlab-com: true
- packages: [Starter]
+ packages: [Free]
url: https://docs.gitlab.com/ee/ci/examples/authenticating-with-hashicorp-vault/index.html
image_url: https://about.gitlab.com/images/12_10/jwt-vault-1.png
published_at: 2020-04-22
diff --git a/spec/frontend/__helpers__/emoji.js b/spec/frontend/__helpers__/emoji.js
index ea6613b53c9..9f9134f6f63 100644
--- a/spec/frontend/__helpers__/emoji.js
+++ b/spec/frontend/__helpers__/emoji.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
import { initEmojiMap, EMOJI_VERSION } from '~/emoji';
+import axios from '~/lib/utils/axios_utils';
export const emojiFixtureMap = {
atom: {
@@ -29,10 +29,6 @@ export const emojiFixtureMap = {
unicodeVersion: '6.0',
description: 'white question mark ornament',
},
-
- // used for regression tests
- // black_heart MUST come before heart
- // custard MUST come before star
black_heart: {
moji: '🖤',
unicodeVersion: '1.1',
@@ -55,34 +51,18 @@ export const emojiFixtureMap = {
},
};
-Object.keys(emojiFixtureMap).forEach((k) => {
- emojiFixtureMap[k].name = k;
- if (!emojiFixtureMap[k].aliases) {
- emojiFixtureMap[k].aliases = [];
- }
-});
+export const mockEmojiData = Object.keys(emojiFixtureMap).reduce((acc, k) => {
+ const { moji: e, unicodeVersion: u, category: c, description: d } = emojiFixtureMap[k];
+ acc[k] = { name: k, e, u, c, d };
-export async function initEmojiMock() {
- const emojiData = Object.fromEntries(
- Object.values(emojiFixtureMap).map((m) => {
- const { name: n, moji: e, unicodeVersion: u, category: c, description: d } = m;
- return [n, { c, e, d, u }];
- }),
- );
+ return acc;
+}, {});
+export async function initEmojiMock(mockData = mockEmojiData) {
const mock = new MockAdapter(axios);
- mock.onGet(`/-/emojis/${EMOJI_VERSION}/emojis.json`).reply(200, JSON.stringify(emojiData));
+ mock.onGet(`/-/emojis/${EMOJI_VERSION}/emojis.json`).reply(200, JSON.stringify(mockData));
await initEmojiMap();
return mock;
}
-
-export function describeEmojiFields(label, tests) {
- describe.each`
- field | accessor
- ${'name'} | ${(e) => e.name}
- ${'alias'} | ${(e) => e.aliases[0]}
- ${'description'} | ${(e) => e.description}
- `(label, tests);
-}
diff --git a/spec/frontend/__helpers__/fake_date.js b/spec/frontend/__helpers__/fake_date/fake_date.js
index 5391ae04797..bc088ad96b6 100644
--- a/spec/frontend/__helpers__/fake_date.js
+++ b/spec/frontend/__helpers__/fake_date/fake_date.js
@@ -1,11 +1,13 @@
// Frida Kahlo's birthday (6 = July)
-export const DEFAULT_ARGS = [2020, 6, 6];
+const DEFAULT_ARGS = [2020, 6, 6];
const RealDate = Date;
const isMocked = (val) => Boolean(val.mock);
-export const createFakeDateClass = (ctorDefault) => {
+const createFakeDateClass = (ctorDefaultParam = []) => {
+ const ctorDefault = ctorDefaultParam.length ? ctorDefaultParam : DEFAULT_ARGS;
+
const FakeDate = new Proxy(RealDate, {
construct: (target, argArray) => {
const ctorArgs = argArray.length ? argArray : ctorDefault;
@@ -39,11 +41,20 @@ export const createFakeDateClass = (ctorDefault) => {
return FakeDate;
};
-export const useFakeDate = (...args) => {
- const FakeDate = createFakeDateClass(args.length ? args : DEFAULT_ARGS);
+const setGlobalDateToFakeDate = (...args) => {
+ const FakeDate = createFakeDateClass(args);
global.Date = FakeDate;
};
-export const useRealDate = () => {
+const setGlobalDateToRealDate = () => {
global.Date = RealDate;
};
+
+// We use commonjs so that the test environment module can pick this up
+// eslint-disable-next-line import/no-commonjs
+module.exports = {
+ setGlobalDateToFakeDate,
+ setGlobalDateToRealDate,
+ createFakeDateClass,
+ RealDate,
+};
diff --git a/spec/frontend/__helpers__/fake_date_spec.js b/spec/frontend/__helpers__/fake_date/fake_date_spec.js
index b3ed13e238a..730765e52d2 100644
--- a/spec/frontend/__helpers__/fake_date_spec.js
+++ b/spec/frontend/__helpers__/fake_date/fake_date_spec.js
@@ -1,15 +1,11 @@
-import { createFakeDateClass, DEFAULT_ARGS, useRealDate } from './fake_date';
+import { createFakeDateClass } from './fake_date';
describe('spec/helpers/fake_date', () => {
describe('createFakeDateClass', () => {
let FakeDate;
- beforeAll(() => {
- useRealDate();
- });
-
beforeEach(() => {
- FakeDate = createFakeDateClass(DEFAULT_ARGS);
+ FakeDate = createFakeDateClass();
});
it('should use default args', () => {
diff --git a/spec/frontend/__helpers__/fake_date/index.js b/spec/frontend/__helpers__/fake_date/index.js
new file mode 100644
index 00000000000..3d1b124ce79
--- /dev/null
+++ b/spec/frontend/__helpers__/fake_date/index.js
@@ -0,0 +1,2 @@
+export * from './fake_date';
+export * from './jest';
diff --git a/spec/frontend/__helpers__/fake_date/jest.js b/spec/frontend/__helpers__/fake_date/jest.js
new file mode 100644
index 00000000000..65e45619049
--- /dev/null
+++ b/spec/frontend/__helpers__/fake_date/jest.js
@@ -0,0 +1,41 @@
+import { createJestExecutionWatcher } from '../jest_execution_watcher';
+import { RealDate, createFakeDateClass } from './fake_date';
+
+const throwInsideExecutionError = (fnName) => {
+ throw new Error(`Cannot call "${fnName}" during test execution (i.e. within "it", "beforeEach", "beforeAll", etc.).
+
+Instead, please move the call to "${fnName}" inside the "describe" block itself.
+
+ describe('', () => {
+ + ${fnName}();
+
+ it('', () => {
+ - ${fnName}();
+ })
+ })
+`);
+};
+
+const isExecutingTest = createJestExecutionWatcher();
+
+export const useDateInScope = (fnName, factory) => {
+ if (isExecutingTest()) {
+ throwInsideExecutionError(fnName);
+ }
+
+ let origDate;
+
+ beforeAll(() => {
+ origDate = global.Date;
+ global.Date = factory();
+ });
+
+ afterAll(() => {
+ global.Date = origDate;
+ });
+};
+
+export const useFakeDate = (...args) =>
+ useDateInScope('useFakeDate', () => createFakeDateClass(args));
+
+export const useRealDate = () => useDateInScope('useRealDate', () => RealDate);
diff --git a/spec/frontend/__helpers__/graphql_helpers.js b/spec/frontend/__helpers__/graphql_helpers.js
new file mode 100644
index 00000000000..63123aa046f
--- /dev/null
+++ b/spec/frontend/__helpers__/graphql_helpers.js
@@ -0,0 +1,14 @@
+/**
+ * Returns a clone of the given object with all __typename keys omitted,
+ * including deeply nested ones.
+ *
+ * Only works with JSON-serializable objects.
+ *
+ * @param {object} An object with __typename keys (e.g., a GraphQL response)
+ * @returns {object} A new object with no __typename keys
+ */
+export const stripTypenames = (object) => {
+ return JSON.parse(
+ JSON.stringify(object, (key, value) => (key === '__typename' ? undefined : value)),
+ );
+};
diff --git a/spec/frontend/__helpers__/graphql_helpers_spec.js b/spec/frontend/__helpers__/graphql_helpers_spec.js
new file mode 100644
index 00000000000..dd23fbbf4e9
--- /dev/null
+++ b/spec/frontend/__helpers__/graphql_helpers_spec.js
@@ -0,0 +1,23 @@
+import { stripTypenames } from './graphql_helpers';
+
+describe('stripTypenames', () => {
+ it.each`
+ input | expected
+ ${{}} | ${{}}
+ ${{ __typename: 'Foo' }} | ${{}}
+ ${{ bar: 'bar', __typename: 'Foo' }} | ${{ bar: 'bar' }}
+ ${{ bar: { __typename: 'Bar' }, __typename: 'Foo' }} | ${{ bar: {} }}
+ ${{ bar: [{ __typename: 'Bar' }], __typename: 'Foo' }} | ${{ bar: [{}] }}
+ ${[]} | ${[]}
+ ${[{ __typename: 'Foo' }]} | ${[{}]}
+ ${[{ bar: [{ a: 1, __typename: 'Bar' }] }]} | ${[{ bar: [{ a: 1 }] }]}
+ `('given $input returns $expected, with all __typename keys removed', ({ input, expected }) => {
+ const actual = stripTypenames(input);
+ expect(actual).toEqual(expected);
+ expect(input).not.toBe(actual);
+ });
+
+ it('given null returns null', () => {
+ expect(stripTypenames(null)).toEqual(null);
+ });
+});
diff --git a/spec/frontend/__helpers__/init_vue_mr_page_helper.js b/spec/frontend/__helpers__/init_vue_mr_page_helper.js
index b9aed63d0f6..ee01e9e6268 100644
--- a/spec/frontend/__helpers__/init_vue_mr_page_helper.js
+++ b/spec/frontend/__helpers__/init_vue_mr_page_helper.js
@@ -1,8 +1,8 @@
import MockAdapter from 'axios-mock-adapter';
-import initMRPage from '~/mr_notes';
import axios from '~/lib/utils/axios_utils';
-import { userDataMock, notesDataMock, noteableDataMock } from '../notes/mock_data';
+import initMRPage from '~/mr_notes';
import diffFileMockData from '../diffs/mock_data/diff_file';
+import { userDataMock, notesDataMock, noteableDataMock } from '../notes/mock_data';
export default function initVueMRPage() {
const mrTestEl = document.createElement('div');
diff --git a/spec/frontend/__helpers__/jest_execution_watcher.js b/spec/frontend/__helpers__/jest_execution_watcher.js
new file mode 100644
index 00000000000..0fc3d330ec3
--- /dev/null
+++ b/spec/frontend/__helpers__/jest_execution_watcher.js
@@ -0,0 +1,12 @@
+export const createJestExecutionWatcher = () => {
+ let isExecuting = false;
+
+ beforeAll(() => {
+ isExecuting = true;
+ });
+ afterAll(() => {
+ isExecuting = false;
+ });
+
+ return () => isExecuting;
+};
diff --git a/spec/frontend/__helpers__/stub_component.js b/spec/frontend/__helpers__/stub_component.js
index 45550450517..96fe3a8bc45 100644
--- a/spec/frontend/__helpers__/stub_component.js
+++ b/spec/frontend/__helpers__/stub_component.js
@@ -1,7 +1,32 @@
+/**
+ * Returns a new object with keys pointing to stubbed methods
+ *
+ * This is helpful for stubbing components like GlModal where it's supported
+ * in the API to call `.show()` and `.hide()` ([Bootstrap Vue docs][1]).
+ *
+ * [1]: https://bootstrap-vue.org/docs/components/modal#using-show-hide-and-toggle-component-methods
+ *
+ * @param {Object} methods - Object whose keys will be in the returned object.
+ */
+const createStubbedMethods = (methods = {}) => {
+ if (!methods) {
+ return {};
+ }
+
+ return Object.keys(methods).reduce(
+ (acc, key) =>
+ Object.assign(acc, {
+ [key]: () => {},
+ }),
+ {},
+ );
+};
+
export function stubComponent(Component, options = {}) {
return {
props: Component.props,
model: Component.model,
+ methods: createStubbedMethods(Component.methods),
// Do not render any slots/scoped slots except default
// This differs from VTU behavior which renders all slots
template: '<div><slot></slot></div>',
diff --git a/spec/frontend/__mocks__/@gitlab/ui.js b/spec/frontend/__mocks__/@gitlab/ui.js
index 7cdecefab05..ecd67247362 100644
--- a/spec/frontend/__mocks__/@gitlab/ui.js
+++ b/spec/frontend/__mocks__/@gitlab/ui.js
@@ -38,7 +38,9 @@ jest.mock('@gitlab/ui/dist/components/base/popover/popover.js', () => ({
required: false,
default: () => [],
},
- ...Object.fromEntries(['target', 'triggers', 'placement'].map((prop) => [prop, {}])),
+ ...Object.fromEntries(
+ ['target', 'triggers', 'placement', 'boundary', 'container'].map((prop) => [prop, {}]),
+ ),
},
render(h) {
return h(
diff --git a/spec/frontend/access_tokens/components/expires_at_field_spec.js b/spec/frontend/access_tokens/components/expires_at_field_spec.js
index cd235d0afa5..4a2815e6931 100644
--- a/spec/frontend/access_tokens/components/expires_at_field_spec.js
+++ b/spec/frontend/access_tokens/components/expires_at_field_spec.js
@@ -1,10 +1,7 @@
import { shallowMount } from '@vue/test-utils';
-import { useFakeDate } from 'helpers/fake_date';
import ExpiresAtField from '~/access_tokens/components/expires_at_field.vue';
describe('~/access_tokens/components/expires_at_field', () => {
- useFakeDate();
-
let wrapper;
const createComponent = () => {
diff --git a/spec/frontend/actioncable_connection_monitor_spec.js b/spec/frontend/actioncable_connection_monitor_spec.js
new file mode 100644
index 00000000000..c68eb53acde
--- /dev/null
+++ b/spec/frontend/actioncable_connection_monitor_spec.js
@@ -0,0 +1,79 @@
+import ConnectionMonitor from '~/actioncable_connection_monitor';
+
+describe('ConnectionMonitor', () => {
+ let monitor;
+
+ beforeEach(() => {
+ monitor = new ConnectionMonitor({});
+ });
+
+ describe('#getPollInterval', () => {
+ beforeEach(() => {
+ Math.originalRandom = Math.random;
+ });
+ afterEach(() => {
+ Math.random = Math.originalRandom;
+ });
+
+ const { staleThreshold, reconnectionBackoffRate } = ConnectionMonitor;
+ const backoffFactor = 1 + reconnectionBackoffRate;
+ const ms = 1000;
+
+ it('uses exponential backoff', () => {
+ Math.random = () => 0;
+
+ monitor.reconnectAttempts = 0;
+ expect(monitor.getPollInterval()).toEqual(staleThreshold * ms);
+
+ monitor.reconnectAttempts = 1;
+ expect(monitor.getPollInterval()).toEqual(staleThreshold * backoffFactor * ms);
+
+ monitor.reconnectAttempts = 2;
+ expect(monitor.getPollInterval()).toEqual(
+ staleThreshold * backoffFactor * backoffFactor * ms,
+ );
+ });
+
+ it('caps exponential backoff after some number of reconnection attempts', () => {
+ Math.random = () => 0;
+ monitor.reconnectAttempts = 42;
+ const cappedPollInterval = monitor.getPollInterval();
+
+ monitor.reconnectAttempts = 9001;
+ expect(monitor.getPollInterval()).toEqual(cappedPollInterval);
+ });
+
+ it('uses 100% jitter when 0 reconnection attempts', () => {
+ Math.random = () => 0;
+ expect(monitor.getPollInterval()).toEqual(staleThreshold * ms);
+
+ Math.random = () => 0.5;
+ expect(monitor.getPollInterval()).toEqual(staleThreshold * 1.5 * ms);
+ });
+
+ it('uses reconnectionBackoffRate for jitter when >0 reconnection attempts', () => {
+ monitor.reconnectAttempts = 1;
+
+ Math.random = () => 0.25;
+ expect(monitor.getPollInterval()).toEqual(
+ staleThreshold * backoffFactor * (1 + reconnectionBackoffRate * 0.25) * ms,
+ );
+
+ Math.random = () => 0.5;
+ expect(monitor.getPollInterval()).toEqual(
+ staleThreshold * backoffFactor * (1 + reconnectionBackoffRate * 0.5) * ms,
+ );
+ });
+
+ it('applies jitter after capped exponential backoff', () => {
+ monitor.reconnectAttempts = 9001;
+
+ Math.random = () => 0;
+ const withoutJitter = monitor.getPollInterval();
+ Math.random = () => 0.5;
+ const withJitter = monitor.getPollInterval();
+
+ expect(withJitter).toBeGreaterThan(withoutJitter);
+ });
+ });
+});
diff --git a/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js b/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js
index 1a3b151afa0..d32e582e498 100644
--- a/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js
+++ b/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js
@@ -1,12 +1,12 @@
+import { GlModal, GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import { GlModal, GlSearchBoxByType } from '@gitlab/ui';
import AddReviewItemsModal from '~/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue';
-import getDiffWithCommit from '../../diffs/mock_data/diff_with_commit';
-import defaultState from '~/add_context_commits_modal/store/state';
-import mutations from '~/add_context_commits_modal/store/mutations';
import * as actions from '~/add_context_commits_modal/store/actions';
+import mutations from '~/add_context_commits_modal/store/mutations';
+import defaultState from '~/add_context_commits_modal/store/state';
+import getDiffWithCommit from '../../diffs/mock_data/diff_with_commit';
const localVue = createLocalVue();
localVue.use(Vuex);
diff --git a/spec/frontend/add_context_commits_modal/components/review_tab_container_spec.js b/spec/frontend/add_context_commits_modal/components/review_tab_container_spec.js
index 4e65713a680..75f1cc41e23 100644
--- a/spec/frontend/add_context_commits_modal/components/review_tab_container_spec.js
+++ b/spec/frontend/add_context_commits_modal/components/review_tab_container_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import ReviewTabContainer from '~/add_context_commits_modal/components/review_tab_container.vue';
import CommitItem from '~/diffs/components/commit_item.vue';
import getDiffWithCommit from '../../diffs/mock_data/diff_with_commit';
diff --git a/spec/frontend/add_context_commits_modal/store/actions_spec.js b/spec/frontend/add_context_commits_modal/store/actions_spec.js
index 3bb3cb68f56..fa4d52cbfbb 100644
--- a/spec/frontend/add_context_commits_modal/store/actions_spec.js
+++ b/spec/frontend/add_context_commits_modal/store/actions_spec.js
@@ -1,7 +1,6 @@
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 {
setBaseConfig,
setTabIndex,
@@ -16,6 +15,7 @@ import {
resetModalState,
} from '~/add_context_commits_modal/store/actions';
import * as types from '~/add_context_commits_modal/store/mutation_types';
+import axios from '~/lib/utils/axios_utils';
describe('AddContextCommitsModalStoreActions', () => {
const contextCommitEndpoint =
diff --git a/spec/frontend/add_context_commits_modal/store/mutations_spec.js b/spec/frontend/add_context_commits_modal/store/mutations_spec.js
index 22f82570ab1..2331a4af1bc 100644
--- a/spec/frontend/add_context_commits_modal/store/mutations_spec.js
+++ b/spec/frontend/add_context_commits_modal/store/mutations_spec.js
@@ -1,6 +1,6 @@
import { TEST_HOST } from 'helpers/test_constants';
-import mutations from '~/add_context_commits_modal/store/mutations';
import * as types from '~/add_context_commits_modal/store/mutation_types';
+import mutations from '~/add_context_commits_modal/store/mutations';
import getDiffWithCommit from '../../diffs/mock_data/diff_with_commit';
describe('AddContextCommitsModalStoreMutations', () => {
diff --git a/spec/frontend/admin/statistics_panel/components/app_spec.js b/spec/frontend/admin/statistics_panel/components/app_spec.js
index a4dcfa1a480..9c424491d04 100644
--- a/spec/frontend/admin/statistics_panel/components/app_spec.js
+++ b/spec/frontend/admin/statistics_panel/components/app_spec.js
@@ -1,12 +1,12 @@
-import Vuex from 'vuex';
+import { GlLoadingIcon } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
-import { GlLoadingIcon } from '@gitlab/ui';
-import axios from '~/lib/utils/axios_utils';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import Vuex from 'vuex';
import StatisticsPanelApp from '~/admin/statistics_panel/components/app.vue';
import statisticsLabels from '~/admin/statistics_panel/constants';
import createStore from '~/admin/statistics_panel/store';
+import axios from '~/lib/utils/axios_utils';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import mockStatistics from '../mock_data';
const localVue = createLocalVue();
diff --git a/spec/frontend/admin/statistics_panel/store/actions_spec.js b/spec/frontend/admin/statistics_panel/store/actions_spec.js
index ecbc823be12..c7481b664b3 100644
--- a/spec/frontend/admin/statistics_panel/store/actions_spec.js
+++ b/spec/frontend/admin/statistics_panel/store/actions_spec.js
@@ -1,10 +1,10 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import * as actions from '~/admin/statistics_panel/store/actions';
import * as types from '~/admin/statistics_panel/store/mutation_types';
import getInitialState from '~/admin/statistics_panel/store/state';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import mockStatistics from '../mock_data';
describe('Admin statistics panel actions', () => {
diff --git a/spec/frontend/admin/statistics_panel/store/getters_spec.js b/spec/frontend/admin/statistics_panel/store/getters_spec.js
index 152d82531ed..6cdd40b1a98 100644
--- a/spec/frontend/admin/statistics_panel/store/getters_spec.js
+++ b/spec/frontend/admin/statistics_panel/store/getters_spec.js
@@ -1,5 +1,5 @@
-import createState from '~/admin/statistics_panel/store/state';
import * as getters from '~/admin/statistics_panel/store/getters';
+import createState from '~/admin/statistics_panel/store/state';
describe('Admin statistics panel getters', () => {
let state;
diff --git a/spec/frontend/admin/statistics_panel/store/mutations_spec.js b/spec/frontend/admin/statistics_panel/store/mutations_spec.js
index 179f38d2bc5..0a3dad09c9a 100644
--- a/spec/frontend/admin/statistics_panel/store/mutations_spec.js
+++ b/spec/frontend/admin/statistics_panel/store/mutations_spec.js
@@ -1,5 +1,5 @@
-import mutations from '~/admin/statistics_panel/store/mutations';
import * as types from '~/admin/statistics_panel/store/mutation_types';
+import mutations from '~/admin/statistics_panel/store/mutations';
import getInitialState from '~/admin/statistics_panel/store/state';
import mockStatistics from '../mock_data';
diff --git a/spec/frontend/admin/users/components/actions/actions_spec.js b/spec/frontend/admin/users/components/actions/actions_spec.js
new file mode 100644
index 00000000000..5e232f34311
--- /dev/null
+++ b/spec/frontend/admin/users/components/actions/actions_spec.js
@@ -0,0 +1,98 @@
+import { GlDropdownItem } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { kebabCase } from 'lodash';
+import { nextTick } from 'vue';
+import Actions from '~/admin/users/components/actions';
+import SharedDeleteAction from '~/admin/users/components/actions/shared/shared_delete_action.vue';
+import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+
+import { CONFIRMATION_ACTIONS, DELETE_ACTIONS } from '../../constants';
+
+describe('Action components', () => {
+ let wrapper;
+
+ const findDropdownItem = () => wrapper.find(GlDropdownItem);
+
+ const initComponent = ({ component, props, stubs = {} } = {}) => {
+ wrapper = shallowMount(component, {
+ propsData: {
+ ...props,
+ },
+ stubs,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('CONFIRMATION_ACTIONS', () => {
+ it.each(CONFIRMATION_ACTIONS)('renders a dropdown item for "%s"', async (action) => {
+ initComponent({
+ component: Actions[capitalizeFirstCharacter(action)],
+ props: {
+ username: 'John Doe',
+ path: '/test',
+ },
+ });
+
+ await nextTick();
+
+ const div = wrapper.find('div');
+ expect(div.attributes('data-path')).toBe('/test');
+ expect(div.attributes('data-modal-attributes')).toContain('John Doe');
+ expect(findDropdownItem().exists()).toBe(true);
+ });
+ });
+
+ describe('LINK_ACTIONS', () => {
+ it.each`
+ action | method
+ ${'Approve'} | ${'put'}
+ ${'Reject'} | ${'delete'}
+ `(
+ 'renders a dropdown item link with method "$method" for "$action"',
+ async ({ action, method }) => {
+ initComponent({
+ component: Actions[action],
+ props: {
+ path: '/test',
+ },
+ });
+
+ await nextTick();
+
+ const item = wrapper.find(GlDropdownItem);
+ expect(item.attributes('href')).toBe('/test');
+ expect(item.attributes('data-method')).toContain(method);
+ },
+ );
+ });
+
+ describe('DELETE_ACTION_COMPONENTS', () => {
+ it.each(DELETE_ACTIONS)('renders a dropdown item for "%s"', async (action) => {
+ initComponent({
+ component: Actions[capitalizeFirstCharacter(action)],
+ props: {
+ username: 'John Doe',
+ paths: {
+ delete: '/delete',
+ block: '/block',
+ },
+ },
+ stubs: { SharedDeleteAction },
+ });
+
+ await nextTick();
+
+ const sharedAction = wrapper.find(SharedDeleteAction);
+
+ expect(sharedAction.attributes('data-block-user-url')).toBe('/block');
+ expect(sharedAction.attributes('data-delete-user-url')).toBe('/delete');
+ expect(sharedAction.attributes('data-gl-modal-action')).toBe(kebabCase(action));
+ expect(sharedAction.attributes('data-username')).toBe('John Doe');
+ expect(findDropdownItem().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/admin/users/components/user_actions_spec.js b/spec/frontend/admin/users/components/user_actions_spec.js
new file mode 100644
index 00000000000..0745d961f25
--- /dev/null
+++ b/spec/frontend/admin/users/components/user_actions_spec.js
@@ -0,0 +1,158 @@
+import { GlDropdownDivider } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Actions from '~/admin/users/components/actions';
+import AdminUserActions from '~/admin/users/components/user_actions.vue';
+import { I18N_USER_ACTIONS } from '~/admin/users/constants';
+import { generateUserPaths } from '~/admin/users/utils';
+import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+
+import { CONFIRMATION_ACTIONS, DELETE_ACTIONS, LINK_ACTIONS, LDAP, EDIT } from '../constants';
+import { users, paths } from '../mock_data';
+
+describe('AdminUserActions component', () => {
+ let wrapper;
+ const user = users[0];
+ const userPaths = generateUserPaths(paths, user.username);
+
+ const findEditButton = () => wrapper.find('[data-testid="edit"]');
+ const findActionsDropdown = () => wrapper.find('[data-testid="actions"');
+ const findDropdownDivider = () => wrapper.find(GlDropdownDivider);
+
+ const initComponent = ({ actions = [] } = {}) => {
+ wrapper = shallowMount(AdminUserActions, {
+ propsData: {
+ user: {
+ ...user,
+ actions,
+ },
+ paths,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('edit button', () => {
+ describe('when the user has an edit action attached', () => {
+ beforeEach(() => {
+ initComponent({ actions: [EDIT] });
+ });
+
+ it('renders the edit button linking to the user edit path', () => {
+ expect(findEditButton().exists()).toBe(true);
+ expect(findEditButton().attributes('href')).toBe(userPaths.edit);
+ });
+ });
+
+ describe('when there is no edit action attached to the user', () => {
+ beforeEach(() => {
+ initComponent({ actions: [] });
+ });
+
+ it('does not render the edit button linking to the user edit path', () => {
+ expect(findEditButton().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('actions dropdown', () => {
+ describe('when there are actions', () => {
+ const actions = [EDIT, ...LINK_ACTIONS];
+
+ beforeEach(() => {
+ initComponent({ actions });
+ });
+
+ it('renders the actions dropdown', () => {
+ expect(findActionsDropdown().exists()).toBe(true);
+ });
+
+ describe('when there are actions that should render as links', () => {
+ beforeEach(() => {
+ initComponent({ actions: LINK_ACTIONS });
+ });
+
+ it.each(LINK_ACTIONS)('renders an action component item for "%s"', (action) => {
+ const component = wrapper.find(Actions[capitalizeFirstCharacter(action)]);
+
+ expect(component.props('path')).toBe(userPaths[action]);
+ expect(component.text()).toBe(I18N_USER_ACTIONS[action]);
+ });
+ });
+
+ describe('when there are actions that require confirmation', () => {
+ beforeEach(() => {
+ initComponent({ actions: CONFIRMATION_ACTIONS });
+ });
+
+ it.each(CONFIRMATION_ACTIONS)('renders an action component item for "%s"', (action) => {
+ const component = wrapper.find(Actions[capitalizeFirstCharacter(action)]);
+
+ expect(component.props('username')).toBe(user.name);
+ expect(component.props('path')).toBe(userPaths[action]);
+ expect(component.text()).toBe(I18N_USER_ACTIONS[action]);
+ });
+ });
+
+ describe('when there is a LDAP action', () => {
+ beforeEach(() => {
+ initComponent({ actions: [LDAP] });
+ });
+
+ it('renders the LDAP dropdown item without a link', () => {
+ const dropdownAction = wrapper.find(`[data-testid="${LDAP}"]`);
+ expect(dropdownAction.exists()).toBe(true);
+ expect(dropdownAction.attributes('href')).toBe(undefined);
+ expect(dropdownAction.text()).toBe(I18N_USER_ACTIONS[LDAP]);
+ });
+ });
+
+ describe('when there is a delete action', () => {
+ beforeEach(() => {
+ initComponent({ actions: [LDAP, ...DELETE_ACTIONS] });
+ });
+
+ it('renders a dropdown divider', () => {
+ expect(findDropdownDivider().exists()).toBe(true);
+ });
+
+ it('only renders delete dropdown items for actions containing the word "delete"', () => {
+ const { length } = wrapper.findAll(`[data-testid*="delete-"]`);
+ expect(length).toBe(DELETE_ACTIONS.length);
+ });
+
+ it.each(DELETE_ACTIONS)('renders a delete action component item for "%s"', (action) => {
+ const component = wrapper.find(Actions[capitalizeFirstCharacter(action)]);
+
+ expect(component.props('username')).toBe(user.name);
+ expect(component.props('paths')).toEqual(userPaths);
+ expect(component.text()).toBe(I18N_USER_ACTIONS[action]);
+ });
+ });
+
+ describe('when there are no delete actions', () => {
+ it('does not render a dropdown divider', () => {
+ expect(findDropdownDivider().exists()).toBe(false);
+ });
+
+ it('does not render a delete dropdown item', () => {
+ const anyDeleteAction = wrapper.find(`[data-testid*="delete-"]`);
+ expect(anyDeleteAction.exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('when there are no actions', () => {
+ beforeEach(() => {
+ initComponent({ actions: [] });
+ });
+
+ it('does not render the actions dropdown', () => {
+ expect(findActionsDropdown().exists()).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/admin/users/components/user_avatar_spec.js b/spec/frontend/admin/users/components/user_avatar_spec.js
index ba4e83690d0..8bbfb89bec1 100644
--- a/spec/frontend/admin/users/components/user_avatar_spec.js
+++ b/spec/frontend/admin/users/components/user_avatar_spec.js
@@ -1,7 +1,10 @@
-import { GlAvatarLink, GlAvatarLabeled, GlBadge } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
+import { GlAvatarLabeled, GlBadge, GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import AdminUserAvatar from '~/admin/users/components/user_avatar.vue';
+import { LENGTH_OF_USER_NOTE_TOOLTIP } from '~/admin/users/constants';
+import { truncate } from '~/lib/utils/text_utility';
import { users, paths } from '../mock_data';
describe('AdminUserAvatar component', () => {
@@ -9,17 +12,25 @@ describe('AdminUserAvatar component', () => {
const user = users[0];
const adminUserPath = paths.adminUser;
+ const findNote = () => wrapper.find(GlIcon);
const findAvatar = () => wrapper.find(GlAvatarLabeled);
- const findAvatarLink = () => wrapper.find(GlAvatarLink);
+ const findUserLink = () => wrapper.find('.js-user-link');
const findAllBadges = () => wrapper.findAll(GlBadge);
+ const findTooltip = () => getBinding(findNote().element, 'gl-tooltip');
const initComponent = (props = {}) => {
- wrapper = mount(AdminUserAvatar, {
+ wrapper = shallowMount(AdminUserAvatar, {
propsData: {
user,
adminUserPath,
...props,
},
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
+ stubs: {
+ GlAvatarLabeled,
+ },
});
};
@@ -33,31 +44,83 @@ describe('AdminUserAvatar component', () => {
initComponent();
});
- it("links to the user's admin path", () => {
- expect(findAvatarLink().attributes()).toMatchObject({
- href: adminUserPath.replace('id', user.username),
+ it('adds a user link hover card', () => {
+ expect(findUserLink().attributes()).toMatchObject({
'data-user-id': user.id.toString(),
'data-username': user.username,
});
});
- it("renders the user's name", () => {
- expect(findAvatar().props('label')).toBe(user.name);
+ it("renders the user's name with an admin path link", () => {
+ const avatar = findAvatar();
+
+ expect(avatar.props('label')).toBe(user.name);
+ expect(avatar.props('labelLink')).toBe(adminUserPath.replace('id', user.username));
});
- it("renders the user's email", () => {
- expect(findAvatar().props('subLabel')).toBe(user.email);
+ it("renders the user's email with a mailto link", () => {
+ const avatar = findAvatar();
+
+ expect(avatar.props('subLabel')).toBe(user.email);
+ expect(avatar.props('subLabelLink')).toBe(`mailto:${user.email}`);
});
it("renders the user's avatar image", () => {
expect(findAvatar().attributes('src')).toBe(user.avatarUrl);
});
+ it('renders a user note icon', () => {
+ expect(findNote().exists()).toBe(true);
+ expect(findNote().props('name')).toBe('document');
+ });
+
+ it("renders the user's note tooltip", () => {
+ const tooltip = findTooltip();
+
+ expect(tooltip).toBeDefined();
+ expect(tooltip.value).toBe(user.note);
+ });
+
it("renders the user's badges", () => {
findAllBadges().wrappers.forEach((badge, idx) => {
expect(badge.text()).toBe(user.badges[idx].text);
expect(badge.props('variant')).toBe(user.badges[idx].variant);
});
});
+
+ describe('and the user note is very long', () => {
+ const noteText = new Array(LENGTH_OF_USER_NOTE_TOOLTIP + 1).join('a');
+
+ beforeEach(() => {
+ initComponent({
+ user: {
+ ...user,
+ note: noteText,
+ },
+ });
+ });
+
+ it("renders a truncated user's note tooltip", () => {
+ const tooltip = findTooltip();
+
+ expect(tooltip).toBeDefined();
+ expect(tooltip.value).toBe(truncate(noteText, LENGTH_OF_USER_NOTE_TOOLTIP));
+ });
+ });
+
+ describe('and the user does not have a note', () => {
+ beforeEach(() => {
+ initComponent({
+ user: {
+ ...user,
+ note: null,
+ },
+ });
+ });
+
+ it('does not render a user note', () => {
+ expect(findNote().exists()).toBe(false);
+ });
+ });
});
});
diff --git a/spec/frontend/admin/users/components/user_date_spec.js b/spec/frontend/admin/users/components/user_date_spec.js
new file mode 100644
index 00000000000..6428b10059b
--- /dev/null
+++ b/spec/frontend/admin/users/components/user_date_spec.js
@@ -0,0 +1,34 @@
+import { shallowMount } from '@vue/test-utils';
+
+import UserDate from '~/admin/users/components/user_date.vue';
+import { users } from '../mock_data';
+
+const mockDate = users[0].createdAt;
+
+describe('FormatDate component', () => {
+ let wrapper;
+
+ const initComponent = (props = {}) => {
+ wrapper = shallowMount(UserDate, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it.each`
+ date | output
+ ${mockDate} | ${'13 Nov, 2020'}
+ ${null} | ${'Never'}
+ ${undefined} | ${'Never'}
+ `('renders $date as $output', ({ date, output }) => {
+ initComponent({ date });
+
+ expect(wrapper.text()).toBe(output);
+ });
+});
diff --git a/spec/frontend/admin/users/components/users_table_spec.js b/spec/frontend/admin/users/components/users_table_spec.js
index b79d2d4d39d..f1fcc20fb65 100644
--- a/spec/frontend/admin/users/components/users_table_spec.js
+++ b/spec/frontend/admin/users/components/users_table_spec.js
@@ -1,8 +1,11 @@
import { GlTable } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import AdminUsersTable from '~/admin/users/components/users_table.vue';
+import AdminUserActions from '~/admin/users/components/user_actions.vue';
import AdminUserAvatar from '~/admin/users/components/user_avatar.vue';
+import AdminUserDate from '~/admin/users/components/user_date.vue';
+import AdminUsersTable from '~/admin/users/components/users_table.vue';
+
import { users, paths } from '../mock_data';
describe('AdminUsersTable component', () => {
@@ -39,18 +42,21 @@ describe('AdminUsersTable component', () => {
initComponent();
});
- it.each`
- key | label
- ${'name'} | ${'Name'}
- ${'projectsCount'} | ${'Projects'}
- ${'createdAt'} | ${'Created on'}
- ${'lastActivityOn'} | ${'Last activity'}
- `('renders users.$key in column $label', ({ key, label }) => {
- expect(getCellByLabel(0, label).text()).toContain(`${user[key]}`);
+ it('renders the projects count', () => {
+ expect(getCellByLabel(0, 'Projects').text()).toContain(`${user.projectsCount}`);
});
- it('renders an AdminUserAvatar component', () => {
- expect(getCellByLabel(0, 'Name').find(AdminUserAvatar).exists()).toBe(true);
+ it('renders the user actions', () => {
+ expect(wrapper.find(AdminUserActions).exists()).toBe(true);
+ });
+
+ it.each`
+ component | label
+ ${AdminUserAvatar} | ${'Name'}
+ ${AdminUserDate} | ${'Created on'}
+ ${AdminUserDate} | ${'Last activity'}
+ `('renders the component for column $label', ({ component, label }) => {
+ expect(getCellByLabel(0, label).find(component).exists()).toBe(true);
});
});
diff --git a/spec/frontend/admin/users/constants.js b/spec/frontend/admin/users/constants.js
new file mode 100644
index 00000000000..60abdc6c248
--- /dev/null
+++ b/spec/frontend/admin/users/constants.js
@@ -0,0 +1,19 @@
+const BLOCK = 'block';
+const UNBLOCK = 'unblock';
+const DELETE = 'delete';
+const DELETE_WITH_CONTRIBUTIONS = 'deleteWithContributions';
+const UNLOCK = 'unlock';
+const ACTIVATE = 'activate';
+const DEACTIVATE = 'deactivate';
+const REJECT = 'reject';
+const APPROVE = 'approve';
+
+export const EDIT = 'edit';
+
+export const LDAP = 'ldapBlocked';
+
+export const LINK_ACTIONS = [APPROVE, REJECT];
+
+export const CONFIRMATION_ACTIONS = [ACTIVATE, BLOCK, DEACTIVATE, UNLOCK, UNBLOCK];
+
+export const DELETE_ACTIONS = [DELETE, DELETE_WITH_CONTRIBUTIONS];
diff --git a/spec/frontend/admin/users/index_spec.js b/spec/frontend/admin/users/index_spec.js
index 171d54c8f4f..20b60bd8640 100644
--- a/spec/frontend/admin/users/index_spec.js
+++ b/spec/frontend/admin/users/index_spec.js
@@ -1,5 +1,5 @@
import { createWrapper } from '@vue/test-utils';
-import initAdminUsers from '~/admin/users';
+import { initAdminUsersApp } from '~/admin/users';
import AdminUsersApp from '~/admin/users/components/app.vue';
import { users, paths } from './mock_data';
@@ -16,7 +16,7 @@ describe('initAdminUsersApp', () => {
document.body.appendChild(el);
- wrapper = createWrapper(initAdminUsers(el));
+ wrapper = createWrapper(initAdminUsersApp(el));
});
afterEach(() => {
diff --git a/spec/frontend/admin/users/mock_data.js b/spec/frontend/admin/users/mock_data.js
index 860994a9152..c3918ef5173 100644
--- a/spec/frontend/admin/users/mock_data.js
+++ b/spec/frontend/admin/users/mock_data.js
@@ -14,6 +14,7 @@ export const users = [
],
projectsCount: 0,
actions: [],
+ note: 'Create per issue #999',
},
];
diff --git a/spec/frontend/alert_management/components/alert_management_empty_state_spec.js b/spec/frontend/alert_management/components/alert_management_empty_state_spec.js
index 509c67743c1..c2bf90e7635 100644
--- a/spec/frontend/alert_management/components/alert_management_empty_state_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_empty_state_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlEmptyState } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import AlertManagementEmptyState from '~/alert_management/components/alert_management_empty_state.vue';
import defaultProvideValues from '../mocks/alerts_provide_config.json';
diff --git a/spec/frontend/alert_management/components/alert_management_list_wrapper_spec.js b/spec/frontend/alert_management/components/alert_management_list_wrapper_spec.js
index 1d79b10a796..bba5fcbbf08 100644
--- a/spec/frontend/alert_management/components/alert_management_list_wrapper_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_list_wrapper_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import AlertManagementList from '~/alert_management/components/alert_management_list_wrapper.vue';
import AlertManagementEmptyState from '~/alert_management/components/alert_management_empty_state.vue';
+import AlertManagementList from '~/alert_management/components/alert_management_list_wrapper.vue';
import AlertManagementTable from '~/alert_management/components/alert_management_table.vue';
import defaultProvideValues from '../mocks/alerts_provide_config.json';
diff --git a/spec/frontend/alert_management/components/alert_management_table_spec.js b/spec/frontend/alert_management/components/alert_management_table_spec.js
index 0cc3d565e10..cea665aa50d 100644
--- a/spec/frontend/alert_management/components/alert_management_table_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_table_spec.js
@@ -1,12 +1,12 @@
-import { mount } from '@vue/test-utils';
import { GlTable, GlAlert, GlLoadingIcon, GlDropdown, GlIcon, GlAvatar } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
-import { visitUrl } from '~/lib/utils/url_utility';
-import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+import mockAlerts from 'jest/vue_shared/alert_details/mocks/alerts.json';
import AlertManagementTable from '~/alert_management/components/alert_management_table.vue';
+import { visitUrl } from '~/lib/utils/url_utility';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
-import mockAlerts from '../mocks/alerts.json';
+import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import defaultProvideValues from '../mocks/alerts_provide_config.json';
jest.mock('~/lib/utils/url_utility', () => ({
diff --git a/spec/frontend/alerts_service_settings/components/__snapshots__/alerts_service_form_spec.js.snap b/spec/frontend/alerts_service_settings/components/__snapshots__/alerts_service_form_spec.js.snap
deleted file mode 100644
index 0d4171a20b3..00000000000
--- a/spec/frontend/alerts_service_settings/components/__snapshots__/alerts_service_form_spec.js.snap
+++ /dev/null
@@ -1,9 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`AlertsServiceForm with default values renders "authorization-key" input 1`] = `"<gl-form-input-stub id=\\"authorization-key\\" readonly=\\"true\\" value=\\"abcedfg123\\"></gl-form-input-stub>"`;
-
-exports[`AlertsServiceForm with default values renders "url" input 1`] = `"<gl-form-input-stub id=\\"url\\" readonly=\\"true\\" value=\\"https://gitlab.com/endpoint-url\\"></gl-form-input-stub>"`;
-
-exports[`AlertsServiceForm with default values renders toggle button 1`] = `"<toggle-button-stub id=\\"activated\\"></toggle-button-stub>"`;
-
-exports[`AlertsServiceForm with default values shows description and docs links 1`] = `"<p><gl-sprintf-stub message=\\"You must provide this URL and authorization key to authorize an external service to send alerts to GitLab. You can provide this URL and key to multiple services. After configuring an external service, alerts from your service will display on the GitLab %{linkStart}Alerts%{linkEnd} page.\\"></gl-sprintf-stub></p><p><gl-sprintf-stub message=\\"Review your external service's documentation to learn where to provide this information to your external service, and the %{linkStart}GitLab documentation%{linkEnd} to learn more about configuring your endpoint.\\"></gl-sprintf-stub></p>"`;
diff --git a/spec/frontend/alerts_service_settings/components/alerts_service_form_spec.js b/spec/frontend/alerts_service_settings/components/alerts_service_form_spec.js
deleted file mode 100644
index 346059ed7be..00000000000
--- a/spec/frontend/alerts_service_settings/components/alerts_service_form_spec.js
+++ /dev/null
@@ -1,152 +0,0 @@
-import { nextTick } from 'vue';
-import axios from 'axios';
-import MockAdapter from 'axios-mock-adapter';
-import { shallowMount } from '@vue/test-utils';
-import { GlModal } from '@gitlab/ui';
-import AlertsServiceForm from '~/alerts_service_settings/components/alerts_service_form.vue';
-import ToggleButton from '~/vue_shared/components/toggle_button.vue';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
-
-jest.mock('~/flash');
-
-const defaultProps = {
- initialAuthorizationKey: 'abcedfg123',
- formPath: 'http://invalid',
- url: 'https://gitlab.com/endpoint-url',
- alertsSetupUrl: 'http://invalid',
- alertsUsageUrl: 'http://invalid',
- initialActivated: false,
- isDisabled: false,
-};
-
-describe('AlertsServiceForm', () => {
- let wrapper;
- let mockAxios;
-
- const createComponent = (props = defaultProps) => {
- wrapper = shallowMount(AlertsServiceForm, {
- propsData: {
- ...defaultProps,
- ...props,
- },
- });
- };
-
- const findUrl = () => wrapper.find('#url');
- const findAuthorizationKey = () => wrapper.find('#authorization-key');
- const findDescription = () => wrapper.find('[data-testid="description"');
-
- beforeEach(() => {
- mockAxios = new MockAdapter(axios);
- });
-
- afterEach(() => {
- wrapper.destroy();
- mockAxios.restore();
- });
-
- describe('with default values', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('renders "url" input', () => {
- expect(findUrl().html()).toMatchSnapshot();
- });
-
- it('renders "authorization-key" input', () => {
- expect(findAuthorizationKey().html()).toMatchSnapshot();
- });
-
- it('renders toggle button', () => {
- expect(wrapper.find(ToggleButton).html()).toMatchSnapshot();
- });
-
- it('shows description and docs links', () => {
- expect(findDescription().element.innerHTML).toMatchSnapshot();
- });
- });
-
- describe('reset key', () => {
- it('updates the authorization key on success', async () => {
- const formPath = 'some/path';
- mockAxios.onPut(formPath).replyOnce(200, { token: 'newToken' });
-
- createComponent({ formPath });
-
- wrapper.find(GlModal).vm.$emit('ok');
- await axios.waitForAll();
-
- expect(findAuthorizationKey().attributes('value')).toBe('newToken');
- });
-
- it('shows flash message on error', () => {
- const formPath = 'some/path';
- mockAxios.onPut(formPath).replyOnce(404);
-
- createComponent({ formPath });
-
- return wrapper.vm.resetKey().then(() => {
- expect(findAuthorizationKey().attributes('value')).toBe(
- defaultProps.initialAuthorizationKey,
- );
- expect(createFlash).toHaveBeenCalled();
- });
- });
- });
-
- describe('activate toggle', () => {
- describe('successfully completes', () => {
- describe.each`
- initialActivated | value
- ${false} | ${true}
- ${true} | ${false}
- `(
- 'when initialActivated=$initialActivated and value=$value',
- ({ initialActivated, value }) => {
- beforeEach(() => {
- const formPath = 'some/path';
- mockAxios
- .onPut(formPath, { service: { active: value } })
- .replyOnce(200, { active: value });
- createComponent({ initialActivated, formPath });
-
- return wrapper.vm.toggleActivated(value);
- });
-
- it(`updates toggle button value to ${value}`, () => {
- expect(wrapper.find(ToggleButton).props('value')).toBe(value);
- });
- },
- );
- });
-
- describe('error is encountered', () => {
- beforeEach(() => {
- const formPath = 'some/path';
- mockAxios.onPut(formPath).replyOnce(500);
- });
-
- it('restores previous value', () => {
- createComponent({ initialActivated: false });
-
- return wrapper.vm.toggleActivated(true).then(() => {
- expect(wrapper.find(ToggleButton).props('value')).toBe(false);
- });
- });
- });
- });
-
- describe('form is disabled', () => {
- beforeEach(() => {
- createComponent({ isDisabled: true });
- });
-
- it('cannot be toggled', () => {
- wrapper.find(ToggleButton).vm.$emit('change');
- return nextTick().then(() => {
- expect(wrapper.find(ToggleButton).props('disabledInput')).toBe(true);
- });
- });
- });
-});
diff --git a/spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_spec.js.snap b/spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_spec.js.snap
deleted file mode 100644
index ef68a6a2c32..00000000000
--- a/spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_spec.js.snap
+++ /dev/null
@@ -1,98 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`AlertsSettingsFormNew with default values renders the initial template 1`] = `
-"<form class=\\"gl-mt-6\\">
- <h5 class=\\"gl-font-lg gl-my-5\\">Add new integrations</h5>
- <div id=\\"integration-type\\" role=\\"group\\" class=\\"form-group gl-form-group\\"><label id=\\"integration-type__BV_label_\\" for=\\"integration-type\\" class=\\"d-block col-form-label\\">1. Select integration type</label>
- <div class=\\"bv-no-focus-ring\\"><select class=\\"gl-form-select mw-100 custom-select\\" id=\\"__BVID__8\\">
- <option value=\\"\\">Select integration type</option>
- <option value=\\"HTTP\\">HTTP Endpoint</option>
- <option value=\\"PROMETHEUS\\">External Prometheus</option>
- </select>
- <!---->
- <!---->
- <!---->
- <!---->
- </div>
- </div>
- <transition-stub css=\\"true\\" enterclass=\\"\\" leaveclass=\\"collapse show\\" entertoclass=\\"collapse show\\" leavetoclass=\\"collapse\\" enteractiveclass=\\"collapsing\\" leaveactiveclass=\\"collapsing\\" class=\\"gl-mt-3\\">
- <div class=\\"collapse\\" style=\\"display: none;\\" id=\\"__BVID__10\\">
- <div>
- <div id=\\"name-integration\\" role=\\"group\\" class=\\"form-group gl-form-group\\"><label id=\\"name-integration__BV_label_\\" for=\\"name-integration\\" class=\\"d-block col-form-label\\">2. Name integration</label>
- <div class=\\"bv-no-focus-ring\\"><input type=\\"text\\" placeholder=\\"Enter integration name\\" class=\\"gl-form-input form-control\\" id=\\"__BVID__15\\">
- <!---->
- <!---->
- <!---->
- </div>
- </div>
- <div id=\\"integration-webhook\\" role=\\"group\\" class=\\"form-group gl-form-group\\"><label id=\\"integration-webhook__BV_label_\\" for=\\"integration-webhook\\" class=\\"d-block col-form-label\\">3. Set up webhook</label>
- <div class=\\"bv-no-focus-ring\\"><span>Utilize the URL and authorization key below to authorize an external service to send alerts to GitLab. Review your external service's documentation to learn where to add these details, and the <a rel=\\"noopener noreferrer\\" target=\\"_blank\\" href=\\"https://docs.gitlab.com/ee/operations/incident_management/alert_integrations.html\\" class=\\"gl-link gl-display-inline-block\\">GitLab documentation</a> to learn more about configuring your endpoint.</span> <label class=\\"gl-display-flex gl-flex-direction-column gl-mb-0 gl-w-max-content gl-my-4 gl-font-weight-normal\\">
- <div class=\\"gl-toggle-wrapper\\"><span class=\\"gl-toggle-label\\">Active</span>
- <!----> <button aria-label=\\"Active\\" type=\\"button\\" class=\\"gl-toggle\\"><span class=\\"toggle-icon\\"><svg data-testid=\\"close-icon\\" aria-hidden=\\"true\\" class=\\"gl-icon s16\\"><use href=\\"#close\\"></use></svg></span></button></div>
- <!---->
- </label>
- <!---->
- <div class=\\"gl-my-4\\"><span class=\\"gl-font-weight-bold\\">
- Webhook URL
- </span>
- <div id=\\"url\\" readonly=\\"readonly\\">
- <div role=\\"group\\" class=\\"input-group\\">
- <!---->
- <!----> <input id=\\"url\\" type=\\"text\\" readonly=\\"readonly\\" class=\\"gl-form-input form-control\\">
- <div class=\\"input-group-append\\"><button title=\\"Copy\\" data-clipboard-text=\\"\\" aria-label=\\"Copy this value\\" type=\\"button\\" class=\\"btn gl-m-0! btn-default btn-md gl-button btn-default-secondary btn-icon\\">
- <!----> <svg data-testid=\\"copy-to-clipboard-icon\\" aria-hidden=\\"true\\" class=\\"gl-button-icon gl-icon s16\\">
- <use href=\\"#copy-to-clipboard\\"></use>
- </svg>
- <!----></button></div>
- <!---->
- </div>
- </div>
- </div>
- <div class=\\"gl-my-4\\"><span class=\\"gl-font-weight-bold\\">
- Authorization key
- </span>
- <div id=\\"authorization-key\\" readonly=\\"readonly\\" class=\\"gl-mb-3\\">
- <div role=\\"group\\" class=\\"input-group\\">
- <!---->
- <!----> <input id=\\"authorization-key\\" type=\\"text\\" readonly=\\"readonly\\" class=\\"gl-form-input form-control\\">
- <div class=\\"input-group-append\\"><button title=\\"Copy\\" data-clipboard-text=\\"\\" aria-label=\\"Copy this value\\" type=\\"button\\" class=\\"btn gl-m-0! btn-default btn-md gl-button btn-default-secondary btn-icon\\">
- <!----> <svg data-testid=\\"copy-to-clipboard-icon\\" aria-hidden=\\"true\\" class=\\"gl-button-icon gl-icon s16\\">
- <use href=\\"#copy-to-clipboard\\"></use>
- </svg>
- <!----></button></div>
- <!---->
- </div>
- </div> <button type=\\"button\\" disabled=\\"disabled\\" class=\\"btn btn-default btn-md disabled gl-button\\">
- <!---->
- <!----> <span class=\\"gl-button-text\\">
- Reset Key
- </span></button>
- <!---->
- </div>
- <!---->
- <!---->
- <!---->
- </div>
- </div>
- <div id=\\"test-integration\\" role=\\"group\\" class=\\"form-group gl-form-group\\"><label id=\\"test-integration__BV_label_\\" for=\\"test-integration\\" class=\\"d-block col-form-label\\">4. Sample alert payload (optional)</label>
- <div class=\\"bv-no-focus-ring\\"><span>Provide an example payload from the monitoring tool you intend to integrate with. This payload can be used to test the integration (optional).</span> <textarea id=\\"test-payload\\" disabled=\\"disabled\\" placeholder=\\"{ &quot;events&quot;: [{ &quot;application&quot;: &quot;Name of application&quot; }] }\\" wrap=\\"soft\\" class=\\"gl-form-input gl-form-textarea gl-my-3 form-control is-valid\\" style=\\"resize: none; overflow-y: scroll;\\"></textarea>
- <!---->
- <!---->
- <!---->
- </div>
- </div>
- <!---->
- <!---->
- </div>
- <div class=\\"gl-display-flex gl-justify-content-start gl-py-3\\"><button data-testid=\\"integration-form-submit\\" type=\\"submit\\" class=\\"btn js-no-auto-disable btn-success btn-md gl-button\\">
- <!---->
- <!----> <span class=\\"gl-button-text\\">Save integration
- </span></button> <button data-testid=\\"integration-test-and-submit\\" type=\\"button\\" disabled=\\"disabled\\" class=\\"btn gl-mx-3 js-no-auto-disable btn-success btn-md disabled gl-button btn-success-secondary\\">
- <!---->
- <!----> <span class=\\"gl-button-text\\">Save and test payload</span></button> <button type=\\"reset\\" class=\\"btn js-no-auto-disable btn-default btn-md gl-button\\">
- <!---->
- <!----> <span class=\\"gl-button-text\\">Cancel</span></button></div>
- </div>
- </transition-stub>
-</form>"
-`;
diff --git a/spec/frontend/alerts_settings/components/__snapshots__/alerts_settings_form_spec.js.snap b/spec/frontend/alerts_settings/components/__snapshots__/alerts_settings_form_spec.js.snap
new file mode 100644
index 00000000000..eb2b82a0211
--- /dev/null
+++ b/spec/frontend/alerts_settings/components/__snapshots__/alerts_settings_form_spec.js.snap
@@ -0,0 +1,406 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`AlertsSettingsForm with default values renders the initial template 1`] = `
+<form
+ class="gl-mt-6"
+>
+ <h5
+ class="gl-font-lg gl-my-5"
+ >
+ Add new integrations
+ </h5>
+
+ <div
+ class="form-group gl-form-group"
+ id="integration-type"
+ role="group"
+ >
+ <label
+ class="d-block col-form-label"
+ for="integration-type"
+ id="integration-type__BV_label_"
+ >
+ 1. Select integration type
+ </label>
+ <div
+ class="bv-no-focus-ring"
+ >
+ <select
+ class="gl-form-select mw-100 custom-select"
+ id="__BVID__8"
+ >
+ <option
+ value=""
+ >
+ Select integration type
+ </option>
+ <option
+ value="HTTP"
+ >
+ HTTP Endpoint
+ </option>
+ <option
+ value="PROMETHEUS"
+ >
+ External Prometheus
+ </option>
+ </select>
+
+ <!---->
+ <!---->
+ <!---->
+ <!---->
+ </div>
+ </div>
+
+ <transition-stub
+ class="gl-mt-3"
+ css="true"
+ enteractiveclass="collapsing"
+ enterclass=""
+ entertoclass="collapse show"
+ leaveactiveclass="collapsing"
+ leaveclass="collapse show"
+ leavetoclass="collapse"
+ >
+ <div
+ class="collapse"
+ id="__BVID__10"
+ style="display: none;"
+ >
+ <div>
+ <div
+ class="form-group gl-form-group"
+ id="name-integration"
+ role="group"
+ >
+ <label
+ class="d-block col-form-label"
+ for="name-integration"
+ id="name-integration__BV_label_"
+ >
+ 2. Name integration
+ </label>
+ <div
+ class="bv-no-focus-ring"
+ >
+ <input
+ class="gl-form-input form-control"
+ id="__BVID__15"
+ placeholder="Enter integration name"
+ type="text"
+ />
+ <!---->
+ <!---->
+ <!---->
+ </div>
+ </div>
+
+ <div
+ class="form-group gl-form-group"
+ id="integration-webhook"
+ role="group"
+ >
+ <label
+ class="d-block col-form-label"
+ for="integration-webhook"
+ id="integration-webhook__BV_label_"
+ >
+ 3. Set up webhook
+ </label>
+ <div
+ class="bv-no-focus-ring"
+ >
+ <span>
+ Utilize the URL and authorization key below to authorize an external service to send alerts to GitLab. Review your external service's documentation to learn where to add these details, and the
+ <a
+ class="gl-link gl-display-inline-block"
+ href="https://docs.gitlab.com/ee/operations/incident_management/alert_integrations.html"
+ rel="noopener noreferrer"
+ target="_blank"
+ >
+ GitLab documentation
+ </a>
+ to learn more about configuring your endpoint.
+ </span>
+
+ <label
+ class="gl-display-flex gl-flex-direction-column gl-mb-0 gl-w-max-content gl-my-4 gl-font-weight-normal"
+ >
+ <span
+ class="gl-toggle-wrapper"
+ >
+ <span
+ class="gl-toggle-label"
+ data-testid="toggle-label"
+ >
+ Active
+ </span>
+
+ <!---->
+
+ <button
+ aria-label="Active"
+ class="gl-toggle"
+ role="switch"
+ type="button"
+ >
+ <span
+ class="toggle-icon"
+ >
+ <svg
+ aria-hidden="true"
+ class="gl-icon s16"
+ data-testid="close-icon"
+ >
+ <use
+ href="#close"
+ />
+ </svg>
+ </span>
+ </button>
+ </span>
+
+ <!---->
+ </label>
+
+ <!---->
+
+ <div
+ class="gl-my-4"
+ >
+ <span
+ class="gl-font-weight-bold"
+ >
+
+ Webhook URL
+
+ </span>
+
+ <div
+ id="url"
+ readonly="readonly"
+ >
+ <div
+ class="input-group"
+ role="group"
+ >
+ <!---->
+ <!---->
+
+ <input
+ class="gl-form-input form-control"
+ id="url"
+ readonly="readonly"
+ type="text"
+ />
+
+ <div
+ class="input-group-append"
+ >
+ <button
+ aria-label="Copy this value"
+ class="btn gl-m-0! btn-default btn-md gl-button btn-default-secondary btn-icon"
+ data-clipboard-text=""
+ title="Copy"
+ type="button"
+ >
+ <!---->
+
+ <svg
+ aria-hidden="true"
+ class="gl-button-icon gl-icon s16"
+ data-testid="copy-to-clipboard-icon"
+ >
+ <use
+ href="#copy-to-clipboard"
+ />
+ </svg>
+
+ <!---->
+ </button>
+ </div>
+ <!---->
+ </div>
+ </div>
+ </div>
+
+ <div
+ class="gl-my-4"
+ >
+ <span
+ class="gl-font-weight-bold"
+ >
+
+ Authorization key
+
+ </span>
+
+ <div
+ class="gl-mb-3"
+ id="authorization-key"
+ readonly="readonly"
+ >
+ <div
+ class="input-group"
+ role="group"
+ >
+ <!---->
+ <!---->
+
+ <input
+ class="gl-form-input form-control"
+ id="authorization-key"
+ readonly="readonly"
+ type="text"
+ />
+
+ <div
+ class="input-group-append"
+ >
+ <button
+ aria-label="Copy this value"
+ class="btn gl-m-0! btn-default btn-md gl-button btn-default-secondary btn-icon"
+ data-clipboard-text=""
+ title="Copy"
+ type="button"
+ >
+ <!---->
+
+ <svg
+ aria-hidden="true"
+ class="gl-button-icon gl-icon s16"
+ data-testid="copy-to-clipboard-icon"
+ >
+ <use
+ href="#copy-to-clipboard"
+ />
+ </svg>
+
+ <!---->
+ </button>
+ </div>
+ <!---->
+ </div>
+ </div>
+
+ <button
+ class="btn btn-default btn-md disabled gl-button"
+ disabled="disabled"
+ type="button"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+
+ Reset Key
+
+ </span>
+ </button>
+
+ <!---->
+ </div>
+ <!---->
+ <!---->
+ <!---->
+ </div>
+ </div>
+
+ <div
+ class="form-group gl-form-group"
+ id="test-integration"
+ role="group"
+ >
+ <label
+ class="d-block col-form-label"
+ for="test-integration"
+ id="test-integration__BV_label_"
+ >
+ 4. Sample alert payload (optional)
+ </label>
+ <div
+ class="bv-no-focus-ring"
+ >
+ <span>
+ Provide an example payload from the monitoring tool you intend to integrate with. This payload can be used to test the integration (optional).
+ </span>
+
+ <textarea
+ class="gl-form-input gl-form-textarea gl-my-3 form-control is-valid"
+ disabled="disabled"
+ id="test-payload"
+ placeholder="{ \\"events\\": [{ \\"application\\": \\"Name of application\\" }] }"
+ style="resize: none; overflow-y: scroll;"
+ wrap="soft"
+ />
+ <!---->
+ <!---->
+ <!---->
+ </div>
+ </div>
+
+ <!---->
+
+ <!---->
+ </div>
+
+ <div
+ class="gl-display-flex gl-justify-content-start gl-py-3"
+ >
+ <button
+ class="btn js-no-auto-disable btn-success btn-md gl-button"
+ data-testid="integration-form-submit"
+ type="submit"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+ Save integration
+
+ </span>
+ </button>
+
+ <button
+ class="btn gl-mx-3 js-no-auto-disable btn-success btn-md disabled gl-button btn-success-secondary"
+ data-testid="integration-test-and-submit"
+ disabled="disabled"
+ type="button"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+ Save and test payload
+ </span>
+ </button>
+
+ <button
+ class="btn js-no-auto-disable btn-default btn-md gl-button"
+ type="reset"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+ Cancel
+ </span>
+ </button>
+ </div>
+ </div>
+ </transition-stub>
+</form>
+`;
diff --git a/spec/frontend/alerts_settings/alert_mapping_builder_spec.js b/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js
index 5d48ff02e35..7e1d1acb62c 100644
--- a/spec/frontend/alerts_settings/alert_mapping_builder_spec.js
+++ b/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js
@@ -1,8 +1,10 @@
import { GlIcon, GlFormInput, GlDropdown, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import AlertMappingBuilder, { i18n } from '~/alerts_settings/components/alert_mapping_builder.vue';
-import gitlabFields from '~/alerts_settings/components/mocks/gitlabFields.json';
import parsedMapping from '~/alerts_settings/components/mocks/parsedMapping.json';
+import * as transformationUtils from '~/alerts_settings/utils/mapping_transformations';
+import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import alertFields from '../mocks/alertFields.json';
describe('AlertMappingBuilder', () => {
let wrapper;
@@ -10,8 +12,9 @@ describe('AlertMappingBuilder', () => {
function mountComponent() {
wrapper = shallowMount(AlertMappingBuilder, {
propsData: {
- payloadFields: parsedMapping.samplePayload.payloadAlerFields.nodes,
- mapping: parsedMapping.storedMapping.nodes,
+ parsedPayload: parsedMapping.samplePayload.payloadAlerFields.nodes,
+ savedMapping: parsedMapping.storedMapping.nodes,
+ alertFields,
},
});
}
@@ -42,52 +45,58 @@ describe('AlertMappingBuilder', () => {
});
it('renders disabled form input for each mapped field', () => {
- gitlabFields.forEach((field, index) => {
+ alertFields.forEach((field, index) => {
const input = findColumnInRow(index + 1, 0).find(GlFormInput);
- expect(input.attributes('value')).toBe(`${field.label} (${field.type.join(' or ')})`);
+ const types = field.types.map((t) => capitalizeFirstCharacter(t.toLowerCase())).join(' or ');
+ expect(input.attributes('value')).toBe(`${field.label} (${types})`);
expect(input.attributes('disabled')).toBe('');
});
});
it('renders right arrow next to each input', () => {
- gitlabFields.forEach((field, index) => {
+ alertFields.forEach((field, index) => {
const arrow = findColumnInRow(index + 1, 1).find('.right-arrow');
expect(arrow.exists()).toBe(true);
});
});
it('renders mapping dropdown for each field', () => {
- gitlabFields.forEach(({ compatibleTypes }, index) => {
+ alertFields.forEach(({ types }, index) => {
const dropdown = findColumnInRow(index + 1, 2).find(GlDropdown);
- const searchBox = dropdown.find(GlSearchBoxByType);
- const dropdownItems = dropdown.findAll(GlDropdownItem);
+ const searchBox = dropdown.findComponent(GlSearchBoxByType);
+ const dropdownItems = dropdown.findAllComponents(GlDropdownItem);
const { nodes } = parsedMapping.samplePayload.payloadAlerFields;
- const numberOfMappingOptions = nodes.filter(({ type }) =>
- type.some((t) => compatibleTypes.includes(t)),
- );
+ const mappingOptions = nodes.filter(({ type }) => types.includes(type));
expect(dropdown.exists()).toBe(true);
expect(searchBox.exists()).toBe(true);
- expect(dropdownItems).toHaveLength(numberOfMappingOptions.length);
+ expect(dropdownItems).toHaveLength(mappingOptions.length);
});
});
it('renders fallback dropdown only for the fields that have fallback', () => {
- gitlabFields.forEach(({ compatibleTypes, numberOfFallbacks }, index) => {
+ alertFields.forEach(({ types, numberOfFallbacks }, index) => {
const dropdown = findColumnInRow(index + 1, 3).find(GlDropdown);
expect(dropdown.exists()).toBe(Boolean(numberOfFallbacks));
if (numberOfFallbacks) {
- const searchBox = dropdown.find(GlSearchBoxByType);
- const dropdownItems = dropdown.findAll(GlDropdownItem);
+ const searchBox = dropdown.findComponent(GlSearchBoxByType);
+ const dropdownItems = dropdown.findAllComponents(GlDropdownItem);
const { nodes } = parsedMapping.samplePayload.payloadAlerFields;
- const numberOfMappingOptions = nodes.filter(({ type }) =>
- type.some((t) => compatibleTypes.includes(t)),
- );
+ const mappingOptions = nodes.filter(({ type }) => types.includes(type));
expect(searchBox.exists()).toBe(Boolean(numberOfFallbacks));
- expect(dropdownItems).toHaveLength(numberOfMappingOptions.length);
+ expect(dropdownItems).toHaveLength(mappingOptions.length);
}
});
});
+
+ it('emits event with selected mapping', () => {
+ const mappingToSave = { fieldName: 'TITLE', mapping: 'PARSED_TITLE' };
+ jest.spyOn(transformationUtils, 'transformForSave').mockReturnValue(mappingToSave);
+ const dropdown = findColumnInRow(1, 2).find(GlDropdown);
+ const option = dropdown.find(GlDropdownItem);
+ option.vm.$emit('click');
+ expect(wrapper.emitted('onMappingUpdate')[0]).toEqual([mappingToSave]);
+ });
});
diff --git a/spec/frontend/alerts_settings/alerts_integrations_list_spec.js b/spec/frontend/alerts_settings/components/alerts_integrations_list_spec.js
index 5a3874d055b..c43d78a1cf3 100644
--- a/spec/frontend/alerts_settings/alerts_integrations_list_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_integrations_list_spec.js
@@ -1,11 +1,11 @@
import { GlTable, GlIcon, GlButton } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { useMockIntersectionObserver } from 'helpers/mock_dom_observer';
-import Tracking from '~/tracking';
import AlertIntegrationsList, {
i18n,
} from '~/alerts_settings/components/alerts_integrations_list.vue';
import { trackAlertIntegrationsViewsOptions } from '~/alerts_settings/constants';
+import Tracking from '~/tracking';
const mockIntegrations = [
{
diff --git a/spec/frontend/alerts_settings/alerts_settings_form_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
index 21cdec6f94c..02229b3d3da 100644
--- a/spec/frontend/alerts_settings/alerts_settings_form_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
@@ -1,4 +1,3 @@
-import { mount } from '@vue/test-utils';
import {
GlForm,
GlFormSelect,
@@ -7,12 +6,15 @@ import {
GlToggle,
GlFormTextarea,
} from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
+import MappingBuilder from '~/alerts_settings/components/alert_mapping_builder.vue';
import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form.vue';
-import { defaultAlertSettingsConfig } from './util';
import { typeSet } from '~/alerts_settings/constants';
+import alertFields from '../mocks/alertFields.json';
+import { defaultAlertSettingsConfig } from './util';
-describe('AlertsSettingsFormNew', () => {
+describe('AlertsSettingsForm', () => {
let wrapper;
const mockToastShow = jest.fn();
@@ -20,6 +22,7 @@ describe('AlertsSettingsFormNew', () => {
data = {},
props = {},
multipleHttpIntegrationsCustomMapping = false,
+ multiIntegrations = true,
} = {}) => {
wrapper = mount(AlertsSettingsForm, {
data() {
@@ -31,8 +34,9 @@ describe('AlertsSettingsFormNew', () => {
...props,
},
provide: {
- glFeatures: { multipleHttpIntegrationsCustomMapping },
...defaultAlertSettingsConfig,
+ glFeatures: { multipleHttpIntegrationsCustomMapping },
+ multiIntegrations,
},
mocks: {
$toast: {
@@ -49,6 +53,7 @@ describe('AlertsSettingsFormNew', () => {
const findFormToggle = () => wrapper.find(GlToggle);
const findTestPayloadSection = () => wrapper.find(`[id = "test-integration"]`);
const findMappingBuilderSection = () => wrapper.find(`[id = "mapping-builder"]`);
+ const findMappingBuilder = () => wrapper.findComponent(MappingBuilder);
const findSubmitButton = () => wrapper.find(`[type = "submit"]`);
const findMultiSupportText = () =>
wrapper.find(`[data-testid="multi-integrations-not-supported"]`);
@@ -63,13 +68,23 @@ describe('AlertsSettingsFormNew', () => {
}
});
+ const selectOptionAtIndex = async (index) => {
+ const options = findSelect().findAll('option');
+ await options.at(index).setSelected();
+ };
+
+ const enableIntegration = (index, value) => {
+ findFormFields().at(index).setValue(value);
+ findFormToggle().trigger('click');
+ };
+
describe('with default values', () => {
beforeEach(() => {
createComponent();
});
it('renders the initial template', () => {
- expect(wrapper.html()).toMatchSnapshot();
+ expect(wrapper.element).toMatchSnapshot();
});
it('render the initial form with only an integration type dropdown', () => {
@@ -80,10 +95,7 @@ describe('AlertsSettingsFormNew', () => {
});
it('shows the rest of the form when the dropdown is used', async () => {
- const options = findSelect().findAll('option');
- await options.at(1).setSelected();
-
- await wrapper.vm.$nextTick();
+ await selectOptionAtIndex(1);
expect(findFormFields().at(0).isVisible()).toBe(true);
});
@@ -96,120 +108,132 @@ describe('AlertsSettingsFormNew', () => {
it('disabled the name input when the selected value is prometheus', async () => {
createComponent();
- const options = findSelect().findAll('option');
- await options.at(2).setSelected();
+ await selectOptionAtIndex(2);
expect(findFormFields().at(0).attributes('disabled')).toBe('disabled');
});
});
describe('submitting integration form', () => {
- it('allows for create-new-integration with the correct form values for HTTP', async () => {
- createComponent();
-
- const options = findSelect().findAll('option');
- await options.at(1).setSelected();
-
- await findFormFields().at(0).setValue('Test integration');
- await findFormToggle().trigger('click');
+ describe('HTTP', () => {
+ it('create', async () => {
+ createComponent();
- await wrapper.vm.$nextTick();
-
- expect(findSubmitButton().exists()).toBe(true);
- expect(findSubmitButton().text()).toBe('Save integration');
+ const integrationName = 'Test integration';
+ await selectOptionAtIndex(1);
+ enableIntegration(0, integrationName);
- findForm().trigger('submit');
+ const submitBtn = findSubmitButton();
+ expect(submitBtn.exists()).toBe(true);
+ expect(submitBtn.text()).toBe('Save integration');
- await wrapper.vm.$nextTick();
+ findForm().trigger('submit');
- expect(wrapper.emitted('create-new-integration')).toBeTruthy();
- expect(wrapper.emitted('create-new-integration')[0]).toEqual([
- { type: typeSet.http, variables: { name: 'Test integration', active: true } },
- ]);
- });
+ expect(wrapper.emitted('create-new-integration')[0]).toEqual([
+ { type: typeSet.http, variables: { name: integrationName, active: true } },
+ ]);
+ });
- it('allows for create-new-integration with the correct form values for PROMETHEUS', async () => {
- createComponent();
+ it('create with custom mapping', async () => {
+ createComponent({
+ multipleHttpIntegrationsCustomMapping: true,
+ multiIntegrations: true,
+ props: { alertFields },
+ });
- const options = findSelect().findAll('option');
- await options.at(2).setSelected();
+ const integrationName = 'Test integration';
+ await selectOptionAtIndex(1);
- await findFormFields().at(0).setValue('Test integration');
- await findFormFields().at(1).setValue('https://test.com');
- await findFormToggle().trigger('click');
+ enableIntegration(0, integrationName);
- await wrapper.vm.$nextTick();
+ const sampleMapping = { field: 'test' };
+ findMappingBuilder().vm.$emit('onMappingUpdate', sampleMapping);
+ findForm().trigger('submit');
- expect(findSubmitButton().exists()).toBe(true);
- expect(findSubmitButton().text()).toBe('Save integration');
+ expect(wrapper.emitted('create-new-integration')[0]).toEqual([
+ {
+ type: typeSet.http,
+ variables: {
+ name: integrationName,
+ active: true,
+ payloadAttributeMappings: sampleMapping,
+ payloadExample: null,
+ },
+ },
+ ]);
+ });
- findForm().trigger('submit');
+ it('update', () => {
+ createComponent({
+ data: {
+ selectedIntegration: typeSet.http,
+ currentIntegration: { id: '1', name: 'Test integration pre' },
+ },
+ props: {
+ loading: false,
+ },
+ });
+ const updatedIntegrationName = 'Test integration post';
+ enableIntegration(0, updatedIntegrationName);
- await wrapper.vm.$nextTick();
+ const submitBtn = findSubmitButton();
+ expect(submitBtn.exists()).toBe(true);
+ expect(submitBtn.text()).toBe('Save integration');
- expect(wrapper.emitted('create-new-integration')).toBeTruthy();
- expect(wrapper.emitted('create-new-integration')[0]).toEqual([
- { type: typeSet.prometheus, variables: { apiUrl: 'https://test.com', active: true } },
- ]);
- });
+ findForm().trigger('submit');
- it('allows for update-integration with the correct form values for HTTP', async () => {
- createComponent({
- data: {
- selectedIntegration: typeSet.http,
- currentIntegration: { id: '1', name: 'Test integration pre' },
- },
- props: {
- loading: false,
- },
+ expect(wrapper.emitted('update-integration')[0]).toEqual([
+ { type: typeSet.http, variables: { name: updatedIntegrationName, active: true } },
+ ]);
});
+ });
- await findFormFields().at(0).setValue('Test integration post');
- await findFormToggle().trigger('click');
+ describe('PROMETHEUS', () => {
+ it('create', async () => {
+ createComponent();
- await wrapper.vm.$nextTick();
+ await selectOptionAtIndex(2);
- expect(findSubmitButton().exists()).toBe(true);
- expect(findSubmitButton().text()).toBe('Save integration');
+ const apiUrl = 'https://test.com';
+ enableIntegration(1, apiUrl);
- findForm().trigger('submit');
+ findFormToggle().trigger('click');
- await wrapper.vm.$nextTick();
+ const submitBtn = findSubmitButton();
+ expect(submitBtn.exists()).toBe(true);
+ expect(submitBtn.text()).toBe('Save integration');
- expect(wrapper.emitted('update-integration')).toBeTruthy();
- expect(wrapper.emitted('update-integration')[0]).toEqual([
- { type: typeSet.http, variables: { name: 'Test integration post', active: true } },
- ]);
- });
+ findForm().trigger('submit');
- it('allows for update-integration with the correct form values for PROMETHEUS', async () => {
- createComponent({
- data: {
- selectedIntegration: typeSet.prometheus,
- currentIntegration: { id: '1', apiUrl: 'https://test-pre.com' },
- },
- props: {
- loading: false,
- },
+ expect(wrapper.emitted('create-new-integration')[0]).toEqual([
+ { type: typeSet.prometheus, variables: { apiUrl, active: true } },
+ ]);
});
- await findFormFields().at(0).setValue('Test integration');
- await findFormFields().at(1).setValue('https://test-post.com');
- await findFormToggle().trigger('click');
-
- await wrapper.vm.$nextTick();
+ it('update', () => {
+ createComponent({
+ data: {
+ selectedIntegration: typeSet.prometheus,
+ currentIntegration: { id: '1', apiUrl: 'https://test-pre.com' },
+ },
+ props: {
+ loading: false,
+ },
+ });
- expect(findSubmitButton().exists()).toBe(true);
- expect(findSubmitButton().text()).toBe('Save integration');
+ const apiUrl = 'https://test-post.com';
+ enableIntegration(1, apiUrl);
- findForm().trigger('submit');
+ const submitBtn = findSubmitButton();
+ expect(submitBtn.exists()).toBe(true);
+ expect(submitBtn.text()).toBe('Save integration');
- await wrapper.vm.$nextTick();
+ findForm().trigger('submit');
- expect(wrapper.emitted('update-integration')).toBeTruthy();
- expect(wrapper.emitted('update-integration')[0]).toEqual([
- { type: typeSet.prometheus, variables: { apiUrl: 'https://test-post.com', active: true } },
- ]);
+ expect(wrapper.emitted('update-integration')[0]).toEqual([
+ { type: typeSet.prometheus, variables: { apiUrl, active: true } },
+ ]);
+ });
});
});
@@ -234,9 +258,10 @@ describe('AlertsSettingsFormNew', () => {
jest.runAllTimers();
await wrapper.vm.$nextTick();
- expect(findJsonTestSubmit().exists()).toBe(true);
- expect(findJsonTestSubmit().text()).toBe('Save and test payload');
- expect(findJsonTestSubmit().props('disabled')).toBe(true);
+ const jsonTestSubmit = findJsonTestSubmit();
+ expect(jsonTestSubmit.exists()).toBe(true);
+ expect(jsonTestSubmit.text()).toBe('Save and test payload');
+ expect(jsonTestSubmit.props('disabled')).toBe(true);
});
it('should allow for the form to be automatically saved if the test payload is successfully submitted', async () => {
@@ -257,6 +282,7 @@ describe('AlertsSettingsFormNew', () => {
currentIntegration: {
type: typeSet.http,
},
+ alertFields,
},
});
});
@@ -329,21 +355,29 @@ describe('AlertsSettingsFormNew', () => {
describe('Mapping builder section', () => {
describe.each`
- featureFlag | integrationOption | visible
- ${true} | ${1} | ${true}
- ${true} | ${2} | ${false}
- ${false} | ${1} | ${false}
- ${false} | ${2} | ${false}
- `('', ({ featureFlag, integrationOption, visible }) => {
+ alertFieldsProvided | multiIntegrations | featureFlag | integrationOption | visible
+ ${true} | ${true} | ${true} | ${1} | ${true}
+ ${true} | ${true} | ${true} | ${2} | ${false}
+ ${true} | ${true} | ${false} | ${1} | ${false}
+ ${true} | ${true} | ${false} | ${2} | ${false}
+ ${true} | ${false} | ${true} | ${1} | ${false}
+ ${false} | ${true} | ${true} | ${1} | ${false}
+ `('', ({ alertFieldsProvided, multiIntegrations, featureFlag, integrationOption, visible }) => {
const visibleMsg = visible ? 'is rendered' : 'is not rendered';
const featureFlagMsg = featureFlag ? 'is enabled' : 'is disabled';
+ const alertFieldsMsg = alertFieldsProvided ? 'are provided' : 'are not provided';
const integrationType = integrationOption === 1 ? typeSet.http : typeSet.prometheus;
- it(`${visibleMsg} when multipleHttpIntegrationsCustomMapping feature flag ${featureFlagMsg} and integration type is ${integrationType}`, async () => {
- createComponent({ multipleHttpIntegrationsCustomMapping: featureFlag });
- const options = findSelect().findAll('option');
- options.at(integrationOption).setSelected();
- await wrapper.vm.$nextTick();
+ it(`${visibleMsg} when multipleHttpIntegrationsCustomMapping feature flag ${featureFlagMsg} and integration type is ${integrationType} and alert fields ${alertFieldsMsg}`, async () => {
+ createComponent({
+ multipleHttpIntegrationsCustomMapping: featureFlag,
+ multiIntegrations,
+ props: {
+ alertFields: alertFieldsProvided ? alertFields : [],
+ },
+ });
+ await selectOptionAtIndex(integrationOption);
+
expect(findMappingBuilderSection().exists()).toBe(visible);
});
});
diff --git a/spec/frontend/alerts_settings/alerts_settings_wrapper_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
index 4d0732ca76c..80293597ab6 100644
--- a/spec/frontend/alerts_settings/alerts_settings_wrapper_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
@@ -1,23 +1,22 @@
-import VueApollo from 'vue-apollo';
+import { GlLoadingIcon } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
+import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
-import waitForPromises from 'helpers/wait_for_promises';
import { useMockIntersectionObserver } from 'helpers/mock_dom_observer';
-import { GlLoadingIcon } from '@gitlab/ui';
-import axios from '~/lib/utils/axios_utils';
-import AlertsSettingsWrapper from '~/alerts_settings/components/alerts_settings_wrapper.vue';
-import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form.vue';
+import waitForPromises from 'helpers/wait_for_promises';
import IntegrationsList from '~/alerts_settings/components/alerts_integrations_list.vue';
-import getIntegrationsQuery from '~/alerts_settings/graphql/queries/get_integrations.query.graphql';
+import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form.vue';
+import AlertsSettingsWrapper from '~/alerts_settings/components/alerts_settings_wrapper.vue';
+import { typeSet } from '~/alerts_settings/constants';
import createHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql';
import createPrometheusIntegrationMutation from '~/alerts_settings/graphql/mutations/create_prometheus_integration.mutation.graphql';
-import updateHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql';
-import updatePrometheusIntegrationMutation from '~/alerts_settings/graphql/mutations/update_prometheus_integration.mutation.graphql';
import destroyHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/destroy_http_integration.mutation.graphql';
import resetHttpTokenMutation from '~/alerts_settings/graphql/mutations/reset_http_token.mutation.graphql';
import resetPrometheusTokenMutation from '~/alerts_settings/graphql/mutations/reset_prometheus_token.mutation.graphql';
-import { typeSet } from '~/alerts_settings/constants';
+import updateHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql';
+import updatePrometheusIntegrationMutation from '~/alerts_settings/graphql/mutations/update_prometheus_integration.mutation.graphql';
+import getIntegrationsQuery from '~/alerts_settings/graphql/queries/get_integrations.query.graphql';
import {
ADD_INTEGRATION_ERROR,
RESET_INTEGRATION_TOKEN_ERROR,
@@ -26,8 +25,7 @@ import {
DELETE_INTEGRATION_ERROR,
} from '~/alerts_settings/utils/error_messages';
import createFlash from '~/flash';
-import { defaultAlertSettingsConfig } from './util';
-import mockIntegrations from './mocks/integrations.json';
+import axios from '~/lib/utils/axios_utils';
import {
createHttpVariables,
updateHttpVariables,
@@ -40,6 +38,8 @@ import {
integrationToDestroy,
destroyIntegrationResponseWithErrors,
} from './mocks/apollo_mock';
+import mockIntegrations from './mocks/integrations.json';
+import { defaultAlertSettingsConfig } from './util';
jest.mock('~/flash');
diff --git a/spec/frontend/alerts_settings/mocks/apollo_mock.js b/spec/frontend/alerts_settings/components/mocks/apollo_mock.js
index e0eba1e8421..e0eba1e8421 100644
--- a/spec/frontend/alerts_settings/mocks/apollo_mock.js
+++ b/spec/frontend/alerts_settings/components/mocks/apollo_mock.js
diff --git a/spec/frontend/alerts_settings/mocks/integrations.json b/spec/frontend/alerts_settings/components/mocks/integrations.json
index b1284fc55a2..b1284fc55a2 100644
--- a/spec/frontend/alerts_settings/mocks/integrations.json
+++ b/spec/frontend/alerts_settings/components/mocks/integrations.json
diff --git a/spec/frontend/alerts_settings/util.js b/spec/frontend/alerts_settings/components/util.js
index 5c07f22f1c9..5c07f22f1c9 100644
--- a/spec/frontend/alerts_settings/util.js
+++ b/spec/frontend/alerts_settings/components/util.js
diff --git a/spec/frontend/alerts_settings/mocks/alertFields.json b/spec/frontend/alerts_settings/mocks/alertFields.json
new file mode 100644
index 00000000000..ffe59dd0c05
--- /dev/null
+++ b/spec/frontend/alerts_settings/mocks/alertFields.json
@@ -0,0 +1,123 @@
+[
+ {
+ "name": "title",
+ "label": "Title",
+ "type": [
+ "string"
+ ],
+ "types": [
+ "string",
+ "number",
+ "datetime"
+ ],
+ "numberOfFallbacks": 1
+ },
+ {
+ "name": "description",
+ "label": "Description",
+ "type": [
+ "string"
+ ],
+ "types": [
+ "string",
+ "number",
+ "datetime"
+ ]
+ },
+ {
+ "name": "start_time",
+ "label": "Start time",
+ "type": [
+ "datetime"
+ ],
+ "types": [
+ "number",
+ "datetime"
+ ]
+ },
+ {
+ "name": "end_time",
+ "label": "End time",
+ "type": [
+ "datetime"
+ ],
+ "types": [
+ "number",
+ "datetime"
+ ]
+ },
+ {
+ "name": "service",
+ "label": "Service",
+ "type": [
+ "string"
+ ],
+ "types": [
+ "string",
+ "number",
+ "datetime"
+ ]
+ },
+ {
+ "name": "monitoring_tool",
+ "label": "Monitoring tool",
+ "type": [
+ "string"
+ ],
+ "types": [
+ "string",
+ "number",
+ "datetime"
+ ]
+ },
+ {
+ "name": "hosts",
+ "label": "Hosts",
+ "type": [
+ "string",
+ "ARRAY"
+ ],
+ "types": [
+ "string",
+ "ARRAY",
+ "number",
+ "datetime"
+ ]
+ },
+ {
+ "name": "severity",
+ "label": "Severity",
+ "type": [
+ "string"
+ ],
+ "types": [
+ "string",
+ "number",
+ "datetime"
+ ]
+ },
+ {
+ "name": "fingerprint",
+ "label": "Fingerprint",
+ "type": [
+ "string"
+ ],
+ "types": [
+ "string",
+ "number",
+ "datetime"
+ ]
+ },
+ {
+ "name": "gitlab_environment_name",
+ "label": "Environment",
+ "type": [
+ "string"
+ ],
+ "types": [
+ "string",
+ "number",
+ "datetime"
+ ]
+ }
+]
diff --git a/spec/frontend/alerts_settings/utils/mapping_transformations_spec.js b/spec/frontend/alerts_settings/utils/mapping_transformations_spec.js
new file mode 100644
index 00000000000..8c1977ffebe
--- /dev/null
+++ b/spec/frontend/alerts_settings/utils/mapping_transformations_spec.js
@@ -0,0 +1,81 @@
+import parsedMapping from '~/alerts_settings/components/mocks/parsedMapping.json';
+import {
+ getMappingData,
+ getPayloadFields,
+ transformForSave,
+} from '~/alerts_settings/utils/mapping_transformations';
+import alertFields from '../mocks/alertFields.json';
+
+describe('Mapping Transformation Utilities', () => {
+ const nameField = {
+ label: 'Name',
+ path: ['alert', 'name'],
+ type: 'string',
+ };
+ const dashboardField = {
+ label: 'Dashboard Id',
+ path: ['alert', 'dashboardId'],
+ type: 'string',
+ };
+
+ describe('getMappingData', () => {
+ it('should return mapping data', () => {
+ const result = getMappingData(
+ alertFields,
+ getPayloadFields(parsedMapping.samplePayload.payloadAlerFields.nodes.slice(0, 3)),
+ parsedMapping.storedMapping.nodes.slice(0, 3),
+ );
+
+ result.forEach((data, index) => {
+ expect(data).toEqual(
+ expect.objectContaining({
+ ...alertFields[index],
+ searchTerm: '',
+ fallbackSearchTerm: '',
+ }),
+ );
+ });
+ });
+ });
+
+ describe('transformForSave', () => {
+ it('should transform mapped data for save', () => {
+ const fieldName = 'title';
+ const mockMappingData = [
+ {
+ name: fieldName,
+ mapping: 'alert_name',
+ mappingFields: getPayloadFields([dashboardField, nameField]),
+ },
+ ];
+ const result = transformForSave(mockMappingData);
+ const { path, type, label } = nameField;
+ expect(result).toEqual([
+ { fieldName: fieldName.toUpperCase(), path, type: type.toUpperCase(), label },
+ ]);
+ });
+
+ it('should return empty array if no mapping provided', () => {
+ const fieldName = 'title';
+ const mockMappingData = [
+ {
+ name: fieldName,
+ mapping: null,
+ mappingFields: getPayloadFields([nameField, dashboardField]),
+ },
+ ];
+ const result = transformForSave(mockMappingData);
+ expect(result).toEqual([]);
+ });
+ });
+
+ describe('getPayloadFields', () => {
+ it('should add name field to each payload field', () => {
+ const result = getPayloadFields([nameField, dashboardField]);
+ expect(result).toEqual([
+ { ...nameField, name: 'alert_name' },
+ { ...dashboardField, name: 'alert_dashboardId' },
+ ]);
+ });
+ });
+});
diff --git a/spec/frontend/analytics/components/activity_chart_spec.js b/spec/frontend/analytics/components/activity_chart_spec.js
index 1f0f9a6c5d7..a6b45ffe20f 100644
--- a/spec/frontend/analytics/components/activity_chart_spec.js
+++ b/spec/frontend/analytics/components/activity_chart_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
+import { shallowMount } from '@vue/test-utils';
import ActivityChart from '~/analytics/product_analytics/components/activity_chart.vue';
describe('Activity Chart Bundle', () => {
diff --git a/spec/frontend/analytics/instance_statistics/components/app_spec.js b/spec/frontend/analytics/instance_statistics/components/app_spec.js
index cc676e86e99..b945cc20bd6 100644
--- a/spec/frontend/analytics/instance_statistics/components/app_spec.js
+++ b/spec/frontend/analytics/instance_statistics/components/app_spec.js
@@ -1,9 +1,9 @@
import { shallowMount } from '@vue/test-utils';
-import InstanceStatisticsApp from '~/analytics/instance_statistics/components/app.vue';
import InstanceCounts from '~/analytics/instance_statistics/components//instance_counts.vue';
+import InstanceStatisticsApp from '~/analytics/instance_statistics/components/app.vue';
import InstanceStatisticsCountChart from '~/analytics/instance_statistics/components/instance_statistics_count_chart.vue';
-import UsersChart from '~/analytics/instance_statistics/components/users_chart.vue';
import ProjectsAndGroupsChart from '~/analytics/instance_statistics/components/projects_and_groups_chart.vue';
+import UsersChart from '~/analytics/instance_statistics/components/users_chart.vue';
describe('InstanceStatisticsApp', () => {
let wrapper;
diff --git a/spec/frontend/analytics/instance_statistics/components/instance_statistics_count_chart_spec.js b/spec/frontend/analytics/instance_statistics/components/instance_statistics_count_chart_spec.js
index a69f3388cbb..e80dcdff426 100644
--- a/spec/frontend/analytics/instance_statistics/components/instance_statistics_count_chart_spec.js
+++ b/spec/frontend/analytics/instance_statistics/components/instance_statistics_count_chart_spec.js
@@ -1,13 +1,13 @@
-import { createLocalVue, shallowMount } from '@vue/test-utils';
-import { GlLineChart } from '@gitlab/ui/dist/charts';
import { GlAlert } from '@gitlab/ui';
+import { GlLineChart } from '@gitlab/ui/dist/charts';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import InstanceStatisticsCountChart from '~/analytics/instance_statistics/components/instance_statistics_count_chart.vue';
import statsQuery from '~/analytics/instance_statistics/graphql/queries/instance_count.query.graphql';
import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
-import { mockCountsData1 } from '../mock_data';
import { mockQueryResponse, mockApolloResponse } from '../apollo_mock_data';
+import { mockCountsData1 } from '../mock_data';
const localVue = createLocalVue();
localVue.use(VueApollo);
diff --git a/spec/frontend/analytics/instance_statistics/components/projects_and_groups_chart_spec.js b/spec/frontend/analytics/instance_statistics/components/projects_and_groups_chart_spec.js
index bf94e476ea3..bbfc65f19b1 100644
--- a/spec/frontend/analytics/instance_statistics/components/projects_and_groups_chart_spec.js
+++ b/spec/frontend/analytics/instance_statistics/components/projects_and_groups_chart_spec.js
@@ -1,15 +1,14 @@
-import { createLocalVue, shallowMount } from '@vue/test-utils';
-import { GlLineChart } from '@gitlab/ui/dist/charts';
import { GlAlert } from '@gitlab/ui';
+import { GlLineChart } from '@gitlab/ui/dist/charts';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
-import { useFakeDate } from 'helpers/fake_date';
import ProjectsAndGroupChart from '~/analytics/instance_statistics/components/projects_and_groups_chart.vue';
-import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
-import projectsQuery from '~/analytics/instance_statistics/graphql/queries/projects.query.graphql';
import groupsQuery from '~/analytics/instance_statistics/graphql/queries/groups.query.graphql';
-import { mockCountsData2, roundedSortedCountsMonthlyChartData2 } from '../mock_data';
+import projectsQuery from '~/analytics/instance_statistics/graphql/queries/projects.query.graphql';
+import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
import { mockQueryResponse } from '../apollo_mock_data';
+import { mockCountsData2, roundedSortedCountsMonthlyChartData2 } from '../mock_data';
const localVue = createLocalVue();
localVue.use(VueApollo);
@@ -45,8 +44,8 @@ describe('ProjectsAndGroupChart', () => {
return shallowMount(ProjectsAndGroupChart, {
props: {
- startDate: useFakeDate(2020, 9, 26),
- endDate: useFakeDate(2020, 10, 1),
+ startDate: new Date(2020, 9, 26),
+ endDate: new Date(2020, 10, 1),
totalDataPoints: mockCountsData2.length,
},
localVue,
diff --git a/spec/frontend/analytics/instance_statistics/components/users_chart_spec.js b/spec/frontend/analytics/instance_statistics/components/users_chart_spec.js
index b9fa30643df..d857b7fae61 100644
--- a/spec/frontend/analytics/instance_statistics/components/users_chart_spec.js
+++ b/spec/frontend/analytics/instance_statistics/components/users_chart_spec.js
@@ -1,18 +1,17 @@
-import { createLocalVue, shallowMount } from '@vue/test-utils';
-import { GlAreaChart } from '@gitlab/ui/dist/charts';
import { GlAlert } from '@gitlab/ui';
+import { GlAreaChart } from '@gitlab/ui/dist/charts';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
-import { useFakeDate } from 'helpers/fake_date';
import UsersChart from '~/analytics/instance_statistics/components/users_chart.vue';
-import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
import usersQuery from '~/analytics/instance_statistics/graphql/queries/users.query.graphql';
+import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
+import { mockQueryResponse } from '../apollo_mock_data';
import {
mockCountsData1,
mockCountsData2,
roundedSortedCountsMonthlyChartData2,
} from '../mock_data';
-import { mockQueryResponse } from '../apollo_mock_data';
const localVue = createLocalVue();
localVue.use(VueApollo);
@@ -31,8 +30,8 @@ describe('UsersChart', () => {
return shallowMount(UsersChart, {
props: {
- startDate: useFakeDate(2020, 9, 26),
- endDate: useFakeDate(2020, 10, 1),
+ startDate: new Date(2020, 9, 26),
+ endDate: new Date(2020, 10, 1),
totalDataPoints: mockCountsData2.length,
},
localVue,
diff --git a/spec/frontend/analytics/shared/components/metric_card_spec.js b/spec/frontend/analytics/shared/components/metric_card_spec.js
index e89d499ed9b..7f587d227ab 100644
--- a/spec/frontend/analytics/shared/components/metric_card_spec.js
+++ b/spec/frontend/analytics/shared/components/metric_card_spec.js
@@ -1,5 +1,5 @@
-import { mount } from '@vue/test-utils';
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import MetricCard from '~/analytics/shared/components/metric_card.vue';
diff --git a/spec/frontend/api/api_utils_spec.js b/spec/frontend/api/api_utils_spec.js
index 3fec26f0149..04be442ef71 100644
--- a/spec/frontend/api/api_utils_spec.js
+++ b/spec/frontend/api/api_utils_spec.js
@@ -20,6 +20,10 @@ describe('~/api/api_utils.js', () => {
);
});
+ it('ensures the URL is prefixed with a /', () => {
+ expect(apiUtils.buildApiUrl('api/:version/projects/:id')).toEqual('/api/v7/projects/:id');
+ });
+
describe('when gon includes a relative_url_root property', () => {
beforeEach(() => {
window.gon.relative_url_root = '/relative/root';
diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js
index 76d67195499..d2522a0124a 100644
--- a/spec/frontend/api_spec.js
+++ b/spec/frontend/api_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
import Api from '~/api';
+import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
describe('Api', () => {
@@ -260,6 +260,28 @@ describe('Api', () => {
});
});
+ describe('groupLabels', () => {
+ it('fetches group labels', (done) => {
+ const options = { params: { search: 'foo' } };
+ const expectedGroup = 'gitlab-org';
+ const expectedUrl = `${dummyUrlRoot}/groups/${expectedGroup}/-/labels`;
+ mock.onGet(expectedUrl).reply(httpStatus.OK, [
+ {
+ id: 1,
+ title: 'Foo Label',
+ },
+ ]);
+
+ Api.groupLabels(expectedGroup, options)
+ .then((res) => {
+ expect(res.length).toBe(1);
+ expect(res[0].title).toBe('Foo Label');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
describe('namespaces', () => {
it('fetches namespaces', (done) => {
const query = 'dummy query';
diff --git a/spec/frontend/artifacts_settings/components/__snapshots__/keep_latest_artifact_checkbox_spec.js.snap b/spec/frontend/artifacts_settings/components/__snapshots__/keep_latest_artifact_checkbox_spec.js.snap
index 9d05e6d99f6..bfe7e40fb32 100644
--- a/spec/frontend/artifacts_settings/components/__snapshots__/keep_latest_artifact_checkbox_spec.js.snap
+++ b/spec/frontend/artifacts_settings/components/__snapshots__/keep_latest_artifact_checkbox_spec.js.snap
@@ -1,29 +1,64 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Keep latest artifact checkbox sets correct setting value in checkbox with query result 1`] = `
+exports[`Keep latest artifact checkbox when application keep latest artifact setting is disabled checkbox is disabled when application setting is disabled 1`] = `
<div>
<!---->
- <gl-form-checkbox-stub
+ <b-form-checkbox-stub
checked="true"
+ class="gl-form-checkbox"
+ disabled="true"
+ plain="true"
+ value="true"
>
- <b
+ <strong
class="gl-mr-3"
>
Keep artifacts from most recent successful jobs
- </b>
+ </strong>
<gl-link-stub
href="/help/ci/pipelines/job_artifacts"
>
More information
</gl-link-stub>
- </gl-form-checkbox-stub>
+
+ <p
+ class="help-text"
+ >
+ This feature is disabled at the instance level.
+ </p>
+ </b-form-checkbox-stub>
+</div>
+`;
+
+exports[`Keep latest artifact checkbox when application keep latest artifact setting is enabled sets correct setting value in checkbox with query result 1`] = `
+<div>
+ <!---->
- <p>
-
- The latest artifacts created by jobs in the most recent successful pipeline will be stored.
-
- </p>
+ <b-form-checkbox-stub
+ checked="true"
+ class="gl-form-checkbox"
+ plain="true"
+ value="true"
+ >
+ <strong
+ class="gl-mr-3"
+ >
+ Keep artifacts from most recent successful jobs
+ </strong>
+
+ <gl-link-stub
+ href="/help/ci/pipelines/job_artifacts"
+ >
+ More information
+ </gl-link-stub>
+
+ <p
+ class="help-text"
+ >
+ The latest artifacts created by jobs in the most recent successful pipeline will be stored.
+ </p>
+ </b-form-checkbox-stub>
</div>
`;
diff --git a/spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js b/spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js
index d7f07526b58..fe2886d6c95 100644
--- a/spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js
+++ b/spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js
@@ -2,14 +2,15 @@ import { GlFormCheckbox, GlLink } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
-import KeepLatestArtifactCheckbox from '~/artifacts_settings/keep_latest_artifact_checkbox.vue';
-import GetKeepLatestArtifactProjectSetting from '~/artifacts_settings/graphql/queries/get_keep_latest_artifact_project_setting.query.graphql';
import UpdateKeepLatestArtifactProjectSetting from '~/artifacts_settings/graphql/mutations/update_keep_latest_artifact_project_setting.mutation.graphql';
+import GetKeepLatestArtifactApplicationSetting from '~/artifacts_settings/graphql/queries/get_keep_latest_artifact_application_setting.query.graphql';
+import GetKeepLatestArtifactProjectSetting from '~/artifacts_settings/graphql/queries/get_keep_latest_artifact_project_setting.query.graphql';
+import KeepLatestArtifactCheckbox from '~/artifacts_settings/keep_latest_artifact_checkbox.vue';
const localVue = createLocalVue();
localVue.use(VueApollo);
-const keepLatestArtifactMock = {
+const keepLatestArtifactProjectMock = {
data: {
project: {
ciCdSettings: { keepLatestArtifact: true },
@@ -17,6 +18,14 @@ const keepLatestArtifactMock = {
},
};
+const keepLatestArtifactApplicationMock = {
+ data: {
+ ciApplicationSettings: {
+ keepLatestArtifact: true,
+ },
+ },
+};
+
const keepLatestArtifactMockResponse = {
data: { ciCdSettingsUpdate: { errors: [], __typename: 'CiCdSettingsUpdatePayload' } },
};
@@ -34,7 +43,12 @@ describe('Keep latest artifact checkbox', () => {
const createComponent = (handlers) => {
requestHandlers = {
- keepLatestArtifactQueryHandler: jest.fn().mockResolvedValue(keepLatestArtifactMock),
+ keepLatestArtifactProjectQueryHandler: jest
+ .fn()
+ .mockResolvedValue(keepLatestArtifactProjectMock),
+ keepLatestArtifactApplicationQueryHandler: jest
+ .fn()
+ .mockResolvedValue(keepLatestArtifactApplicationMock),
keepLatestArtifactMutationHandler: jest
.fn()
.mockResolvedValue(keepLatestArtifactMockResponse),
@@ -42,7 +56,11 @@ describe('Keep latest artifact checkbox', () => {
};
apolloProvider = createMockApollo([
- [GetKeepLatestArtifactProjectSetting, requestHandlers.keepLatestArtifactQueryHandler],
+ [GetKeepLatestArtifactProjectSetting, requestHandlers.keepLatestArtifactProjectQueryHandler],
+ [
+ GetKeepLatestArtifactApplicationSetting,
+ requestHandlers.keepLatestArtifactApplicationQueryHandler,
+ ],
[UpdateKeepLatestArtifactProjectSetting, requestHandlers.keepLatestArtifactMutationHandler],
]);
@@ -51,38 +69,74 @@ describe('Keep latest artifact checkbox', () => {
fullPath,
helpPagePath,
},
+ stubs: {
+ GlFormCheckbox,
+ },
localVue,
apolloProvider,
});
};
- beforeEach(() => {
- createComponent();
- });
-
afterEach(() => {
wrapper.destroy();
wrapper = null;
apolloProvider = null;
});
- it('displays the checkbox and the help link', () => {
- expect(findCheckbox().exists()).toBe(true);
- expect(findHelpLink().exists()).toBe(true);
- });
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('displays the checkbox and the help link', () => {
+ expect(findCheckbox().exists()).toBe(true);
+ expect(findHelpLink().exists()).toBe(true);
+ });
- it('sets correct setting value in checkbox with query result', async () => {
- await wrapper.vm.$nextTick();
+ it('calls mutation on artifact setting change with correct payload', () => {
+ findCheckbox().vm.$emit('change', false);
- expect(wrapper.element).toMatchSnapshot();
+ expect(requestHandlers.keepLatestArtifactMutationHandler).toHaveBeenCalledWith({
+ fullPath,
+ keepLatestArtifact: false,
+ });
+ });
});
- it('calls mutation on artifact setting change with correct payload', () => {
- findCheckbox().vm.$emit('change', false);
+ describe('when application keep latest artifact setting is enabled', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('sets correct setting value in checkbox with query result', async () => {
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('checkbox is enabled when application setting is enabled', async () => {
+ await wrapper.vm.$nextTick();
+
+ expect(findCheckbox().attributes('disabled')).toBeUndefined();
+ });
+ });
- expect(requestHandlers.keepLatestArtifactMutationHandler).toHaveBeenCalledWith({
- fullPath,
- keepLatestArtifact: false,
+ describe('when application keep latest artifact setting is disabled', () => {
+ it('checkbox is disabled when application setting is disabled', async () => {
+ createComponent({
+ keepLatestArtifactApplicationQueryHandler: jest.fn().mockResolvedValue({
+ data: {
+ ciApplicationSettings: {
+ keepLatestArtifact: false,
+ },
+ },
+ }),
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.element).toMatchSnapshot();
+ expect(findCheckbox().attributes('disabled')).toBe('true');
});
});
});
diff --git a/spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js b/spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js
index 98c8ed4b95d..b77def195b6 100644
--- a/spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js
+++ b/spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js
@@ -1,17 +1,17 @@
-import { mount } from '@vue/test-utils';
import { GlAlert, GlButton } from '@gitlab/ui';
-import { nextTick } from 'vue';
import { within } from '@testing-library/dom';
+import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import Tracking from '~/tracking';
import RecoveryCodes, {
i18n,
} from '~/authentication/two_factor_auth/components/recovery_codes.vue';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import {
RECOVERY_CODE_DOWNLOAD_FILENAME,
COPY_KEYBOARD_SHORTCUT,
} from '~/authentication/two_factor_auth/constants';
+import Tracking from '~/tracking';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { codes, codesFormattedString, codesDownloadHref, profileAccountPath } from '../mock_data';
describe('RecoveryCodes', () => {
diff --git a/spec/frontend/authentication/two_factor_auth/index_spec.js b/spec/frontend/authentication/two_factor_auth/index_spec.js
index b181170b0a1..f5345139021 100644
--- a/spec/frontend/authentication/two_factor_auth/index_spec.js
+++ b/spec/frontend/authentication/two_factor_auth/index_spec.js
@@ -1,8 +1,8 @@
-import { createWrapper } from '@vue/test-utils';
import { getByTestId, fireEvent } from '@testing-library/dom';
-import * as urlUtils from '~/lib/utils/url_utility';
+import { createWrapper } from '@vue/test-utils';
import { initRecoveryCodes, initClose2faSuccessMessage } from '~/authentication/two_factor_auth';
import RecoveryCodes from '~/authentication/two_factor_auth/components/recovery_codes.vue';
+import * as urlUtils from '~/lib/utils/url_utility';
import { codesJsonString, codes, profileAccountPath } from './mock_data';
describe('initRecoveryCodes', () => {
diff --git a/spec/frontend/avatar_helper_spec.js b/spec/frontend/avatar_helper_spec.js
index c4da7189751..91bf8e28774 100644
--- a/spec/frontend/avatar_helper_spec.js
+++ b/spec/frontend/avatar_helper_spec.js
@@ -1,5 +1,4 @@
import { TEST_HOST } from 'spec/test_constants';
-import { getFirstCharacterCapitalized } from '~/lib/utils/text_utility';
import {
DEFAULT_SIZE_CLASS,
IDENTICON_BG_COUNT,
@@ -8,6 +7,7 @@ import {
getIdenticonBackgroundClass,
getIdenticonTitle,
} from '~/helpers/avatar_helper';
+import { getFirstCharacterCapitalized } from '~/lib/utils/text_utility';
function matchAll(str) {
return new RegExp(`^${str}$`);
diff --git a/spec/frontend/awards_handler_spec.js b/spec/frontend/awards_handler_spec.js
index e9482ffbd3d..edd17cfd810 100644
--- a/spec/frontend/awards_handler_spec.js
+++ b/spec/frontend/awards_handler_spec.js
@@ -1,10 +1,10 @@
+import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import Cookies from 'js-cookie';
-import MockAdapter from 'axios-mock-adapter';
import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_frame';
-import axios from '~/lib/utils/axios_utils';
import loadAwardsHandler from '~/awards_handler';
import { EMOJI_VERSION } from '~/emoji';
+import axios from '~/lib/utils/axios_utils';
window.gl = window.gl || {};
window.gon = window.gon || {};
@@ -53,6 +53,12 @@ describe('AwardsHandler', () => {
d: 'smiling face with sunglasses',
u: '6.0',
},
+ grey_question: {
+ c: 'symbols',
+ e: '❔',
+ d: 'white question mark ornament',
+ u: '6.0',
+ },
};
preloadFixtures('snippets/show.html');
@@ -285,16 +291,6 @@ describe('AwardsHandler', () => {
expect($('.js-emoji-menu-search').val()).toBe('');
});
- it('should fuzzy filter the emoji', async () => {
- await openAndWaitForEmojiMenu();
-
- awardsHandler.searchEmojis('sgls');
-
- expect($('[data-name=angel]').is(':visible')).toBe(false);
- expect($('[data-name=anger]').is(':visible')).toBe(false);
- expect($('[data-name=sunglasses]').is(':visible')).toBe(true);
- });
-
it('should filter by emoji description', async () => {
await openAndWaitForEmojiMenu();
diff --git a/spec/frontend/badges/components/badge_form_spec.js b/spec/frontend/badges/components/badge_form_spec.js
index 34c65d51115..e375fcb4705 100644
--- a/spec/frontend/badges/components/badge_form_spec.js
+++ b/spec/frontend/badges/components/badge_form_spec.js
@@ -1,11 +1,11 @@
-import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
-import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
+import Vue from 'vue';
import { DUMMY_IMAGE_URL, TEST_HOST } from 'helpers/test_constants';
-import axios from '~/lib/utils/axios_utils';
-import store from '~/badges/store';
-import createEmptyBadge from '~/badges/empty_badge';
+import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import BadgeForm from '~/badges/components/badge_form.vue';
+import createEmptyBadge from '~/badges/empty_badge';
+import store from '~/badges/store';
+import axios from '~/lib/utils/axios_utils';
// avoid preview background process
BadgeForm.methods.debouncedPreview = () => {};
diff --git a/spec/frontend/badges/components/badge_list_row_spec.js b/spec/frontend/badges/components/badge_list_row_spec.js
index c559c979cb3..372663017e2 100644
--- a/spec/frontend/badges/components/badge_list_row_spec.js
+++ b/spec/frontend/badges/components/badge_list_row_spec.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
+import BadgeListRow from '~/badges/components/badge_list_row.vue';
import { GROUP_BADGE, PROJECT_BADGE } from '~/badges/constants';
import store from '~/badges/store';
-import BadgeListRow from '~/badges/components/badge_list_row.vue';
import { createDummyBadge } from '../dummy_badge';
describe('BadgeListRow component', () => {
diff --git a/spec/frontend/badges/components/badge_list_spec.js b/spec/frontend/badges/components/badge_list_spec.js
index da59e13e1e9..6cc90c6de46 100644
--- a/spec/frontend/badges/components/badge_list_spec.js
+++ b/spec/frontend/badges/components/badge_list_spec.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
+import BadgeList from '~/badges/components/badge_list.vue';
import { GROUP_BADGE, PROJECT_BADGE } from '~/badges/constants';
import store from '~/badges/store';
-import BadgeList from '~/badges/components/badge_list.vue';
import { createDummyBadge } from '../dummy_badge';
describe('BadgeList component', () => {
diff --git a/spec/frontend/badges/components/badge_settings_spec.js b/spec/frontend/badges/components/badge_settings_spec.js
index 769be7cb1bd..0c29379763e 100644
--- a/spec/frontend/badges/components/badge_settings_spec.js
+++ b/spec/frontend/badges/components/badge_settings_spec.js
@@ -1,10 +1,10 @@
-import Vuex from 'vuex';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui';
-import store from '~/badges/store';
-import BadgeSettings from '~/badges/components/badge_settings.vue';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
import BadgeList from '~/badges/components/badge_list.vue';
import BadgeListRow from '~/badges/components/badge_list_row.vue';
+import BadgeSettings from '~/badges/components/badge_settings.vue';
+import store from '~/badges/store';
import { createDummyBadge } from '../dummy_badge';
const localVue = createLocalVue();
diff --git a/spec/frontend/badges/store/actions_spec.js b/spec/frontend/badges/store/actions_spec.js
index 52ae29affac..75699f24463 100644
--- a/spec/frontend/badges/store/actions_spec.js
+++ b/spec/frontend/badges/store/actions_spec.js
@@ -1,10 +1,10 @@
import MockAdapter from 'axios-mock-adapter';
-import { TEST_HOST } from 'spec/test_constants';
import testAction from 'helpers/vuex_action_helper';
-import axios from '~/lib/utils/axios_utils';
+import { TEST_HOST } from 'spec/test_constants';
import actions, { transformBackendBadge } from '~/badges/store/actions';
import mutationTypes from '~/badges/store/mutation_types';
import createState from '~/badges/store/state';
+import axios from '~/lib/utils/axios_utils';
import { createDummyBadge, createDummyBadgeResponse } from '../dummy_badge';
describe('Badges store actions', () => {
diff --git a/spec/frontend/batch_comments/components/draft_note_spec.js b/spec/frontend/batch_comments/components/draft_note_spec.js
index ae7134b63c8..c2d488a465e 100644
--- a/spec/frontend/batch_comments/components/draft_note_spec.js
+++ b/spec/frontend/batch_comments/components/draft_note_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
import { getByRole } from '@testing-library/dom';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
import DraftNote from '~/batch_comments/components/draft_note.vue';
import { createStore } from '~/batch_comments/stores';
import NoteableNote from '~/notes/components/noteable_note.vue';
@@ -21,14 +21,11 @@ describe('Batch comments draft note component', () => {
const getList = () => getByRole(wrapper.element, 'list');
- const createComponent = (propsData = { draft }, features = {}) => {
+ const createComponent = (propsData = { draft }) => {
wrapper = shallowMount(localVue.extend(DraftNote), {
store,
propsData,
localVue,
- provide: {
- glFeatures: { multilineComments: true, ...features },
- },
});
jest.spyOn(wrapper.vm.$store, 'dispatch').mockImplementation();
@@ -145,16 +142,14 @@ describe('Batch comments draft note component', () => {
describe('multiline comments', () => {
describe.each`
- desc | props | features | event | expectedCalls
- ${'with `draft.position`'} | ${draftWithLineRange} | ${{}} | ${'mouseenter'} | ${[['setSelectedCommentPositionHover', LINE_RANGE]]}
- ${'with `draft.position`'} | ${draftWithLineRange} | ${{}} | ${'mouseleave'} | ${[['setSelectedCommentPositionHover']]}
- ${'with `draft.position`'} | ${draftWithLineRange} | ${{ multilineComments: false }} | ${'mouseenter'} | ${[]}
- ${'with `draft.position`'} | ${draftWithLineRange} | ${{ multilineComments: false }} | ${'mouseleave'} | ${[]}
- ${'without `draft.position`'} | ${{}} | ${{}} | ${'mouseenter'} | ${[]}
- ${'without `draft.position`'} | ${{}} | ${{}} | ${'mouseleave'} | ${[]}
- `('$desc and features $features', ({ props, event, features, expectedCalls }) => {
+ desc | props | event | expectedCalls
+ ${'with `draft.position`'} | ${draftWithLineRange} | ${'mouseenter'} | ${[['setSelectedCommentPositionHover', LINE_RANGE]]}
+ ${'with `draft.position`'} | ${draftWithLineRange} | ${'mouseleave'} | ${[['setSelectedCommentPositionHover']]}
+ ${'without `draft.position`'} | ${{}} | ${'mouseenter'} | ${[]}
+ ${'without `draft.position`'} | ${{}} | ${'mouseleave'} | ${[]}
+ `('$desc', ({ props, event, expectedCalls }) => {
beforeEach(() => {
- createComponent({ draft: { ...draft, ...props } }, features);
+ createComponent({ draft: { ...draft, ...props } });
jest.spyOn(store, 'dispatch');
});
diff --git a/spec/frontend/batch_comments/components/preview_item_spec.js b/spec/frontend/batch_comments/components/preview_item_spec.js
index 173b2710a30..08167a94068 100644
--- a/spec/frontend/batch_comments/components/preview_item_spec.js
+++ b/spec/frontend/batch_comments/components/preview_item_spec.js
@@ -56,17 +56,30 @@ describe('Batch comments draft preview item component', () => {
createComponent(false, {
file_path: 'index.js',
file_hash: 'abc',
- position: { new_line: 1 },
+ position: {
+ line_range: {
+ start: {
+ new_line: 1,
+ type: 'new',
+ },
+ },
+ },
});
- expect(vm.$el.querySelector('.bold').textContent).toContain(':1');
+ expect(vm.$el.querySelector('.bold').textContent).toContain(':+1');
});
it('renders old line position', () => {
createComponent(false, {
file_path: 'index.js',
file_hash: 'abc',
- position: { old_line: 2 },
+ position: {
+ line_range: {
+ start: {
+ old_line: 2,
+ },
+ },
+ },
});
expect(vm.$el.querySelector('.bold').textContent).toContain(':2');
diff --git a/spec/frontend/batch_comments/components/publish_dropdown_spec.js b/spec/frontend/batch_comments/components/publish_dropdown_spec.js
index f235867f002..bd8091c20e0 100644
--- a/spec/frontend/batch_comments/components/publish_dropdown_spec.js
+++ b/spec/frontend/batch_comments/components/publish_dropdown_spec.js
@@ -1,6 +1,6 @@
-import Vuex from 'vuex';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
import PreviewDropdown from '~/batch_comments/components/preview_dropdown.vue';
import { createStore } from '~/mr_notes/stores';
import '~/behaviors/markdown/render_gfm';
diff --git a/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js b/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
index 3ad131de24e..885e02ef60f 100644
--- a/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
+++ b/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
-import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'helpers/test_constants';
+import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/batch_comments/stores/modules/batch_comments/actions';
import axios from '~/lib/utils/axios_utils';
diff --git a/spec/frontend/batch_comments/stores/modules/batch_comments/mutations_spec.js b/spec/frontend/batch_comments/stores/modules/batch_comments/mutations_spec.js
index 1406f66fd10..fe01de638c2 100644
--- a/spec/frontend/batch_comments/stores/modules/batch_comments/mutations_spec.js
+++ b/spec/frontend/batch_comments/stores/modules/batch_comments/mutations_spec.js
@@ -1,6 +1,6 @@
-import createState from '~/batch_comments/stores/modules/batch_comments/state';
-import mutations from '~/batch_comments/stores/modules/batch_comments/mutations';
import * as types from '~/batch_comments/stores/modules/batch_comments/mutation_types';
+import mutations from '~/batch_comments/stores/modules/batch_comments/mutations';
+import createState from '~/batch_comments/stores/modules/batch_comments/state';
describe('Batch comments mutations', () => {
let state;
diff --git a/spec/frontend/behaviors/autosize_spec.js b/spec/frontend/behaviors/autosize_spec.js
index 352bd8a0ed0..a9dbee7fd08 100644
--- a/spec/frontend/behaviors/autosize_spec.js
+++ b/spec/frontend/behaviors/autosize_spec.js
@@ -1,12 +1,20 @@
import '~/behaviors/autosize';
-function load() {
- document.dispatchEvent(new Event('DOMContentLoaded'));
-}
-
jest.mock('~/helpers/startup_css_helper', () => {
return {
- waitForCSSLoaded: jest.fn().mockImplementation((cb) => cb.apply()),
+ waitForCSSLoaded: jest.fn().mockImplementation((cb) => {
+ // This is a hack:
+ // autosize.js will execute and modify the DOM
+ // whenever waitForCSSLoaded calls its callback function.
+ // This setTimeout is here because everything within setTimeout will be queued
+ // as async code until the current call stack is executed.
+ // If we would not do this, the mock for waitForCSSLoaded would call its callback
+ // before the fixture in the beforeEach is set and the Test would fail.
+ // more on this here: https://johnresig.com/blog/how-javascript-timers-work/
+ setTimeout(() => {
+ cb.apply();
+ }, 0);
+ }),
};
});
@@ -16,9 +24,15 @@ describe('Autosize behavior', () => {
});
it('is applied to the textarea', () => {
- load();
-
- const textarea = document.querySelector('textarea');
- expect(textarea.classList).toContain('js-autosize-initialized');
+ // This is the second part of the Hack:
+ // Because we are forcing the mock for WaitForCSSLoaded and the very end of our callstack
+ // to call its callback. This querySelector needs to go to the very end of our callstack
+ // as well, if we would not have this setTimeout Function here, the querySelector
+ // would run before the mockImplementation called its callBack Function
+ // the DOM Manipulation didn't happen yet and the test would fail.
+ setTimeout(() => {
+ const textarea = document.querySelector('textarea');
+ expect(textarea.classList).toContain('js-autosize-initialized');
+ }, 0);
});
});
diff --git a/spec/frontend/behaviors/copy_as_gfm_spec.js b/spec/frontend/behaviors/copy_as_gfm_spec.js
index 16ea4ba8624..acff990e84a 100644
--- a/spec/frontend/behaviors/copy_as_gfm_spec.js
+++ b/spec/frontend/behaviors/copy_as_gfm_spec.js
@@ -1,5 +1,5 @@
-import * as commonUtils from '~/lib/utils/common_utils';
import initCopyAsGFM, { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm';
+import * as commonUtils from '~/lib/utils/common_utils';
describe('CopyAsGFM', () => {
describe('CopyAsGFM.pasteGFM', () => {
diff --git a/spec/frontend/behaviors/gl_emoji_spec.js b/spec/frontend/behaviors/gl_emoji_spec.js
index 6e476d84501..286ed269421 100644
--- a/spec/frontend/behaviors/gl_emoji_spec.js
+++ b/spec/frontend/behaviors/gl_emoji_spec.js
@@ -1,10 +1,10 @@
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
-import axios from '~/lib/utils/axios_utils';
-import { initEmojiMap, EMOJI_VERSION } from '~/emoji';
import installGlEmojiElement from '~/behaviors/gl_emoji';
+import { initEmojiMap, EMOJI_VERSION } from '~/emoji';
import * as EmojiUnicodeSupport from '~/emoji/support';
+import axios from '~/lib/utils/axios_utils';
jest.mock('~/emoji/support');
diff --git a/spec/frontend/blob/balsamiq/balsamiq_viewer_spec.js b/spec/frontend/blob/balsamiq/balsamiq_viewer_spec.js
index 09c49617bc5..d7531d15b9a 100644
--- a/spec/frontend/blob/balsamiq/balsamiq_viewer_spec.js
+++ b/spec/frontend/blob/balsamiq/balsamiq_viewer_spec.js
@@ -1,7 +1,7 @@
import sqljs from 'sql.js';
import ClassSpecHelper from 'helpers/class_spec_helper';
-import axios from '~/lib/utils/axios_utils';
import BalsamiqViewer from '~/blob/balsamiq/balsamiq_viewer';
+import axios from '~/lib/utils/axios_utils';
jest.mock('sql.js');
diff --git a/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap b/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap
index b54efb93bc9..31fb6addcac 100644
--- a/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap
+++ b/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap
@@ -9,7 +9,7 @@ exports[`Blob Header Default Actions rendering matches the snapshot 1`] = `
/>
<div
- class="gl-display-none gl-display-sm-flex"
+ class="gl-display-none gl-sm-display-flex"
>
<viewer-switcher-stub
value="simple"
diff --git a/spec/frontend/blob/components/blob_content_error_spec.js b/spec/frontend/blob/components/blob_content_error_spec.js
index 95686be8294..0f5885c2acf 100644
--- a/spec/frontend/blob/components/blob_content_error_spec.js
+++ b/spec/frontend/blob/components/blob_content_error_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import BlobContentError from '~/blob/components/blob_content_error.vue';
import { BLOB_RENDER_ERRORS } from '~/blob/components/constants';
diff --git a/spec/frontend/blob/components/blob_content_spec.js b/spec/frontend/blob/components/blob_content_spec.js
index 3db95e5ad3f..8450c6b9332 100644
--- a/spec/frontend/blob/components/blob_content_spec.js
+++ b/spec/frontend/blob/components/blob_content_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import BlobContent from '~/blob/components/blob_content.vue';
import BlobContentError from '~/blob/components/blob_content_error.vue';
import {
@@ -7,6 +7,7 @@ import {
BLOB_RENDER_EVENT_SHOW_SOURCE,
BLOB_RENDER_ERRORS,
} from '~/blob/components/constants';
+import { RichViewer, SimpleViewer } from '~/vue_shared/components/blob_viewers';
import {
Blob,
RichViewerMock,
@@ -14,7 +15,6 @@ import {
RichBlobContentMock,
SimpleBlobContentMock,
} from './mock_data';
-import { RichViewer, SimpleViewer } from '~/vue_shared/components/blob_viewers';
describe('Blob Content component', () => {
let wrapper;
diff --git a/spec/frontend/blob/components/blob_edit_header_spec.js b/spec/frontend/blob/components/blob_edit_header_spec.js
index 22e8e6d986c..ac3080c65a5 100644
--- a/spec/frontend/blob/components/blob_edit_header_spec.js
+++ b/spec/frontend/blob/components/blob_edit_header_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlFormInput, GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import BlobEditHeader from '~/blob/components/blob_edit_header.vue';
describe('Blob Header Editing', () => {
diff --git a/spec/frontend/blob/components/blob_header_default_actions_spec.js b/spec/frontend/blob/components/blob_header_default_actions_spec.js
index e2c73a5d5d9..bce65899c43 100644
--- a/spec/frontend/blob/components/blob_header_default_actions_spec.js
+++ b/spec/frontend/blob/components/blob_header_default_actions_spec.js
@@ -1,5 +1,5 @@
-import { mount } from '@vue/test-utils';
import { GlButtonGroup, GlButton } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import BlobHeaderActions from '~/blob/components/blob_header_default_actions.vue';
import {
BTN_COPY_CONTENTS_TITLE,
diff --git a/spec/frontend/blob/components/blob_header_filepath_spec.js b/spec/frontend/blob/components/blob_header_filepath_spec.js
index 7b8b5050486..d935f73c0d1 100644
--- a/spec/frontend/blob/components/blob_header_filepath_spec.js
+++ b/spec/frontend/blob/components/blob_header_filepath_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
import BlobHeaderFilepath from '~/blob/components/blob_header_filepath.vue';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { Blob as MockBlob } from './mock_data';
-import { numberToHumanSize } from '~/lib/utils/number_utils';
jest.mock('~/lib/utils/number_utils', () => ({
numberToHumanSize: jest.fn(() => 'a lot'),
diff --git a/spec/frontend/blob/components/blob_header_spec.js b/spec/frontend/blob/components/blob_header_spec.js
index b3f80183f6b..865e8ab1124 100644
--- a/spec/frontend/blob/components/blob_header_spec.js
+++ b/spec/frontend/blob/components/blob_header_spec.js
@@ -1,8 +1,8 @@
import { shallowMount, mount } from '@vue/test-utils';
import BlobHeader from '~/blob/components/blob_header.vue';
-import ViewerSwitcher from '~/blob/components/blob_header_viewer_switcher.vue';
import DefaultActions from '~/blob/components/blob_header_default_actions.vue';
import BlobFilepath from '~/blob/components/blob_header_filepath.vue';
+import ViewerSwitcher from '~/blob/components/blob_header_viewer_switcher.vue';
import { Blob } from './mock_data';
diff --git a/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js b/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js
index cf1101bc22c..9a560ec11f7 100644
--- a/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js
+++ b/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js
@@ -1,5 +1,5 @@
-import { mount } from '@vue/test-utils';
import { GlButtonGroup, GlButton } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import BlobHeaderViewerSwitcher from '~/blob/components/blob_header_viewer_switcher.vue';
import {
RICH_BLOB_VIEWER,
diff --git a/spec/frontend/blob/notebook/notebook_viever_spec.js b/spec/frontend/blob/notebook/notebook_viever_spec.js
index f6a926a5ecb..604104bb31f 100644
--- a/spec/frontend/blob/notebook/notebook_viever_spec.js
+++ b/spec/frontend/blob/notebook/notebook_viever_spec.js
@@ -1,9 +1,9 @@
-import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
-import axios from '~/lib/utils/axios_utils';
import component from '~/blob/notebook/notebook_viewer.vue';
+import axios from '~/lib/utils/axios_utils';
import NotebookLab from '~/notebook/index.vue';
describe('iPython notebook renderer', () => {
diff --git a/spec/frontend/blob/pdf/pdf_viewer_spec.js b/spec/frontend/blob/pdf/pdf_viewer_spec.js
index 0eea3aea639..e332ea49fa6 100644
--- a/spec/frontend/blob/pdf/pdf_viewer_spec.js
+++ b/spec/frontend/blob/pdf/pdf_viewer_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { FIXTURES_PATH } from 'spec/test_constants';
import component from '~/blob/pdf/pdf_viewer.vue';
diff --git a/spec/frontend/blob/pipeline_tour_success_modal_spec.js b/spec/frontend/blob/pipeline_tour_success_modal_spec.js
index d4562019302..f4af57de41f 100644
--- a/spec/frontend/blob/pipeline_tour_success_modal_spec.js
+++ b/spec/frontend/blob/pipeline_tour_success_modal_spec.js
@@ -1,8 +1,8 @@
+import { GlSprintf, GlModal, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Cookies from 'js-cookie';
-import { GlSprintf, GlModal, GlLink } from '@gitlab/ui';
-import { mockTracking, triggerEvent, unmockTracking } from 'helpers/tracking_helper';
import { stubComponent } from 'helpers/stub_component';
+import { mockTracking, triggerEvent, unmockTracking } from 'helpers/tracking_helper';
import pipelineTourSuccess from '~/blob/pipeline_tour_success_modal.vue';
import modalProps from './pipeline_tour_success_mock_data';
diff --git a/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js b/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js
index e55b8e4af24..7e13994f2b7 100644
--- a/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js
+++ b/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js
@@ -1,6 +1,6 @@
+import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { mockTracking, unmockTracking, triggerEvent } from 'helpers/tracking_helper';
-import { GlButton } from '@gitlab/ui';
import Popover from '~/blob/suggest_gitlab_ci_yml/components/popover.vue';
import * as utils from '~/lib/utils/common_utils';
diff --git a/spec/frontend/blob/utils_spec.js b/spec/frontend/blob/utils_spec.js
index fa83690c104..3ff2e47e0b6 100644
--- a/spec/frontend/blob/utils_spec.js
+++ b/spec/frontend/blob/utils_spec.js
@@ -1,5 +1,5 @@
-import Editor from '~/editor/editor_lite';
import * as utils from '~/blob/utils';
+import Editor from '~/editor/editor_lite';
jest.mock('~/editor/editor_lite');
diff --git a/spec/frontend/blob/viewer/index_spec.js b/spec/frontend/blob/viewer/index_spec.js
index 4a5eb31602d..7449de48ec0 100644
--- a/spec/frontend/blob/viewer/index_spec.js
+++ b/spec/frontend/blob/viewer/index_spec.js
@@ -1,7 +1,7 @@
/* eslint-disable no-new */
-import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter';
+import $ from 'jquery';
import { setTestTimeout } from 'helpers/timeout';
import BlobViewer from '~/blob/viewer/index';
import axios from '~/lib/utils/axios_utils';
diff --git a/spec/frontend/blob_edit/edit_blob_spec.js b/spec/frontend/blob_edit/edit_blob_spec.js
index 7927c14d2ac..3134feedcf3 100644
--- a/spec/frontend/blob_edit/edit_blob_spec.js
+++ b/spec/frontend/blob_edit/edit_blob_spec.js
@@ -1,8 +1,8 @@
import waitForPromises from 'helpers/wait_for_promises';
import EditBlob from '~/blob_edit/edit_blob';
import EditorLite from '~/editor/editor_lite';
-import { EditorMarkdownExtension } from '~/editor/extensions/editor_markdown_ext';
import { FileTemplateExtension } from '~/editor/extensions/editor_file_template_ext';
+import { EditorMarkdownExtension } from '~/editor/extensions/editor_markdown_ext';
jest.mock('~/editor/editor_lite');
jest.mock('~/editor/extensions/editor_markdown_ext');
diff --git a/spec/frontend/boards/board_list_deprecated_spec.js b/spec/frontend/boards/board_list_deprecated_spec.js
index 393d7f954b1..b71564f7858 100644
--- a/spec/frontend/boards/board_list_deprecated_spec.js
+++ b/spec/frontend/boards/board_list_deprecated_spec.js
@@ -1,17 +1,16 @@
/* global List */
/* global ListIssue */
-
-import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
+import Vue from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import axios from '~/lib/utils/axios_utils';
-import eventHub from '~/boards/eventhub';
import BoardList from '~/boards/components/board_list_deprecated.vue';
+import eventHub from '~/boards/eventhub';
+import store from '~/boards/stores';
+import boardsStore from '~/boards/stores/boards_store';
+import axios from '~/lib/utils/axios_utils';
import '~/boards/models/issue';
import '~/boards/models/list';
import { listObj, boardsMockInterceptor } from './mock_data';
-import store from '~/boards/stores';
-import boardsStore from '~/boards/stores/boards_store';
const createComponent = ({ done, listIssueProps = {}, componentProps = {}, listProps = {} }) => {
const el = document.createElement('div');
diff --git a/spec/frontend/boards/board_list_helper.js b/spec/frontend/boards/board_list_helper.js
index f82b1f7ed5c..915b470df8d 100644
--- a/spec/frontend/boards/board_list_helper.js
+++ b/spec/frontend/boards/board_list_helper.js
@@ -1,17 +1,15 @@
/* global List */
/* global ListIssue */
-
import MockAdapter from 'axios-mock-adapter';
-import Vue from 'vue';
import Sortable from 'sortablejs';
-import axios from '~/lib/utils/axios_utils';
+import Vue from 'vue';
import BoardList from '~/boards/components/board_list_deprecated.vue';
-
import '~/boards/models/issue';
import '~/boards/models/list';
-import { listObj, boardsMockInterceptor } from './mock_data';
import store from '~/boards/stores';
import boardsStore from '~/boards/stores/boards_store';
+import axios from '~/lib/utils/axios_utils';
+import { listObj, boardsMockInterceptor } from './mock_data';
window.Sortable = Sortable;
diff --git a/spec/frontend/boards/board_list_spec.js b/spec/frontend/boards/board_list_spec.js
index 1b62f25044e..7ed20f20882 100644
--- a/spec/frontend/boards/board_list_spec.js
+++ b/spec/frontend/boards/board_list_spec.js
@@ -1,11 +1,11 @@
+import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex';
import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_frame';
-import { createLocalVue, mount } from '@vue/test-utils';
-import eventHub from '~/boards/eventhub';
-import BoardList from '~/boards/components/board_list.vue';
import BoardCard from '~/boards/components/board_card.vue';
-import { mockList, mockIssuesByListId, issues, mockIssues } from './mock_data';
+import BoardList from '~/boards/components/board_list.vue';
+import eventHub from '~/boards/eventhub';
import defaultState from '~/boards/stores/state';
+import { mockList, mockIssuesByListId, issues, mockIssues } from './mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
diff --git a/spec/frontend/boards/board_new_issue_deprecated_spec.js b/spec/frontend/boards/board_new_issue_deprecated_spec.js
index 8236b468189..1a29f680166 100644
--- a/spec/frontend/boards/board_new_issue_deprecated_spec.js
+++ b/spec/frontend/boards/board_new_issue_deprecated_spec.js
@@ -1,11 +1,11 @@
/* global List */
-import Vue from 'vue';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
+import Vue from 'vue';
import boardNewIssue from '~/boards/components/board_new_issue_deprecated.vue';
import boardsStore from '~/boards/stores/boards_store';
+import axios from '~/lib/utils/axios_utils';
import '~/boards/models/list';
import { listObj, boardsMockInterceptor } from './mock_data';
diff --git a/spec/frontend/boards/boards_store_spec.js b/spec/frontend/boards/boards_store_spec.js
index f1d249ff069..02881333273 100644
--- a/spec/frontend/boards/boards_store_spec.js
+++ b/spec/frontend/boards/boards_store_spec.js
@@ -1,12 +1,12 @@
import AxiosMockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
-import axios from '~/lib/utils/axios_utils';
-import boardsStore from '~/boards/stores/boards_store';
import eventHub from '~/boards/eventhub';
-import { listObj, listObjDuplicate } from './mock_data';
import ListIssue from '~/boards/models/issue';
import List from '~/boards/models/list';
+import boardsStore from '~/boards/stores/boards_store';
+import axios from '~/lib/utils/axios_utils';
+import { listObj, listObjDuplicate } from './mock_data';
jest.mock('js-cookie');
diff --git a/spec/frontend/boards/boards_util_spec.js b/spec/frontend/boards/boards_util_spec.js
new file mode 100644
index 00000000000..0feb1411003
--- /dev/null
+++ b/spec/frontend/boards/boards_util_spec.js
@@ -0,0 +1,17 @@
+import { transformNotFilters } from '~/boards/boards_util';
+
+describe('transformNotFilters', () => {
+ const filters = {
+ 'not[labelName]': ['label'],
+ 'not[assigneeUsername]': 'assignee',
+ };
+
+ it('formats not filters, transforms epicId to fullEpicId', () => {
+ const result = transformNotFilters(filters);
+
+ expect(result).toEqual({
+ labelName: ['label'],
+ assigneeUsername: 'assignee',
+ });
+ });
+});
diff --git a/spec/frontend/boards/components/board_assignee_dropdown_spec.js b/spec/frontend/boards/components/board_assignee_dropdown_spec.js
deleted file mode 100644
index e52c14f9783..00000000000
--- a/spec/frontend/boards/components/board_assignee_dropdown_spec.js
+++ /dev/null
@@ -1,380 +0,0 @@
-import { mount, createLocalVue } from '@vue/test-utils';
-import {
- GlDropdownItem,
- GlAvatarLink,
- GlAvatarLabeled,
- GlSearchBoxByType,
- GlLoadingIcon,
-} from '@gitlab/ui';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import VueApollo from 'vue-apollo';
-import BoardAssigneeDropdown from '~/boards/components/board_assignee_dropdown.vue';
-import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue';
-import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dropdown.vue';
-import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
-import store from '~/boards/stores';
-import getIssueParticipants from '~/vue_shared/components/sidebar/queries/getIssueParticipants.query.graphql';
-import searchUsers from '~/boards/graphql/users_search.query.graphql';
-import { participants } from '../mock_data';
-
-const localVue = createLocalVue();
-
-localVue.use(VueApollo);
-
-describe('BoardCardAssigneeDropdown', () => {
- let wrapper;
- let fakeApollo;
- let getIssueParticipantsSpy;
- let getSearchUsersSpy;
- let dispatchSpy;
-
- const iid = '111';
- const activeIssueName = 'test';
- const anotherIssueName = 'hello';
-
- const createComponent = (search = '', loading = false) => {
- wrapper = mount(BoardAssigneeDropdown, {
- data() {
- return {
- search,
- selected: [],
- participants,
- };
- },
- store,
- provide: {
- canUpdate: true,
- rootPath: '',
- },
- mocks: {
- $apollo: {
- queries: {
- participants: {
- loading,
- },
- },
- },
- },
- });
- };
-
- const createComponentWithApollo = (search = '') => {
- fakeApollo = createMockApollo([
- [getIssueParticipants, getIssueParticipantsSpy],
- [searchUsers, getSearchUsersSpy],
- ]);
- wrapper = mount(BoardAssigneeDropdown, {
- localVue,
- apolloProvider: fakeApollo,
- data() {
- return {
- search,
- selected: [],
- participants,
- };
- },
- store,
- provide: {
- canUpdate: true,
- rootPath: '',
- },
- });
- };
-
- const unassign = async () => {
- wrapper.find('[data-testid="unassign"]').trigger('click');
-
- await wrapper.vm.$nextTick();
- };
-
- const openDropdown = async () => {
- wrapper.find('[data-testid="edit-button"]').trigger('click');
-
- await wrapper.vm.$nextTick();
- };
-
- const findByText = (text) => {
- return wrapper.findAll(GlDropdownItem).wrappers.find((node) => node.text().indexOf(text) === 0);
- };
-
- const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
-
- beforeEach(() => {
- store.state.activeId = '1';
- store.state.issues = {
- 1: {
- iid,
- assignees: [{ username: activeIssueName, name: activeIssueName, id: activeIssueName }],
- },
- };
-
- dispatchSpy = jest.spyOn(store, 'dispatch').mockResolvedValue();
- });
-
- afterEach(() => {
- window.gon = {};
- jest.restoreAllMocks();
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- describe('when mounted', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it.each`
- text
- ${anotherIssueName}
- ${activeIssueName}
- `('finds item with $text', ({ text }) => {
- const item = findByText(text);
-
- expect(item.exists()).toBe(true);
- });
-
- it('renders gl-avatar-link in gl-dropdown-item', () => {
- const item = findByText('hello');
-
- expect(item.find(GlAvatarLink).exists()).toBe(true);
- });
-
- it('renders gl-avatar-labeled in gl-avatar-link', () => {
- const item = findByText('hello');
-
- expect(item.find(GlAvatarLink).find(GlAvatarLabeled).exists()).toBe(true);
- });
- });
-
- describe('when selected users are present', () => {
- it('renders a divider', () => {
- createComponent();
-
- expect(wrapper.find('[data-testid="selected-user-divider"]').exists()).toBe(true);
- });
- });
-
- describe('when collapsed', () => {
- it('renders IssuableAssignees', () => {
- createComponent();
-
- expect(wrapper.find(IssuableAssignees).isVisible()).toBe(true);
- expect(wrapper.find(MultiSelectDropdown).isVisible()).toBe(false);
- });
- });
-
- describe('when dropdown is open', () => {
- beforeEach(async () => {
- createComponent();
-
- await openDropdown();
- });
-
- it('shows assignees dropdown', async () => {
- expect(wrapper.find(IssuableAssignees).isVisible()).toBe(false);
- expect(wrapper.find(MultiSelectDropdown).isVisible()).toBe(true);
- });
-
- it('shows the issue returned as the activeIssue', async () => {
- expect(findByText(activeIssueName).props('isChecked')).toBe(true);
- });
-
- describe('when "Unassign" is clicked', () => {
- it('unassigns assignees', async () => {
- await unassign();
-
- expect(findByText('Unassign').props('isChecked')).toBe(true);
- });
- });
-
- describe('when an unselected item is clicked', () => {
- beforeEach(async () => {
- await unassign();
- });
-
- it('assigns assignee in the dropdown', async () => {
- wrapper.find('[data-testid="item_test"]').trigger('click');
-
- await wrapper.vm.$nextTick();
-
- expect(findByText(activeIssueName).props('isChecked')).toBe(true);
- });
-
- it('calls setAssignees with username list', async () => {
- wrapper.find('[data-testid="item_test"]').trigger('click');
-
- await wrapper.vm.$nextTick();
-
- document.body.click();
-
- await wrapper.vm.$nextTick();
-
- expect(store.dispatch).toHaveBeenCalledWith('setAssignees', [activeIssueName]);
- });
- });
-
- describe('when the user off clicks', () => {
- beforeEach(async () => {
- await unassign();
-
- document.body.click();
-
- await wrapper.vm.$nextTick();
- });
-
- it('calls setAssignees with username list', async () => {
- expect(store.dispatch).toHaveBeenCalledWith('setAssignees', []);
- });
-
- it('closes the dropdown', async () => {
- expect(wrapper.find(IssuableAssignees).isVisible()).toBe(true);
- });
- });
- });
-
- it('renders divider after unassign', () => {
- createComponent();
-
- expect(wrapper.find('[data-testid="unassign-divider"]').exists()).toBe(true);
- });
-
- it.each`
- assignees | expected
- ${[{ id: 5, username: '', name: '' }]} | ${'Assignee'}
- ${[{ id: 6, username: '', name: '' }, { id: 7, username: '', name: '' }]} | ${'2 Assignees'}
- `(
- 'when assignees have a length of $assignees.length, it renders $expected',
- ({ assignees, expected }) => {
- store.state.issues['1'].assignees = assignees;
-
- createComponent();
-
- expect(wrapper.find(BoardEditableItem).props('title')).toBe(expected);
- },
- );
-
- describe('when participants is loading', () => {
- beforeEach(() => {
- createComponent('', true);
- });
-
- it('finds a loading icon in the dropdown', () => {
- expect(findLoadingIcon().exists()).toBe(true);
- });
- });
-
- describe('when participants is loading is false', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('does not find GlLoading icon in the dropdown', () => {
- expect(findLoadingIcon().exists()).toBe(false);
- });
-
- it('finds at least 1 GlDropdownItem', () => {
- expect(wrapper.findAll(GlDropdownItem).length).toBeGreaterThan(0);
- });
- });
-
- describe('Apollo', () => {
- beforeEach(() => {
- getIssueParticipantsSpy = jest.fn().mockResolvedValue({
- data: {
- issue: {
- participants: {
- nodes: [
- {
- username: 'participant',
- name: 'participant',
- webUrl: '',
- avatarUrl: '',
- id: '',
- },
- ],
- },
- },
- },
- });
- getSearchUsersSpy = jest.fn().mockResolvedValue({
- data: {
- users: {
- nodes: [{ username: 'root', name: 'root', webUrl: '', avatarUrl: '', id: '' }],
- },
- },
- });
- });
-
- describe('when search is empty', () => {
- beforeEach(() => {
- createComponentWithApollo();
- });
-
- it('calls getIssueParticipants', async () => {
- jest.runOnlyPendingTimers();
- await wrapper.vm.$nextTick();
-
- expect(getIssueParticipantsSpy).toHaveBeenCalledWith({ id: 'gid://gitlab/Issue/111' });
- });
- });
-
- describe('when search is not empty', () => {
- beforeEach(() => {
- createComponentWithApollo('search term');
- });
-
- it('calls searchUsers', async () => {
- jest.runOnlyPendingTimers();
- await wrapper.vm.$nextTick();
-
- expect(getSearchUsersSpy).toHaveBeenCalledWith({ search: 'search term' });
- });
- });
- });
-
- it('finds GlSearchBoxByType', async () => {
- createComponent();
-
- await openDropdown();
-
- expect(wrapper.find(GlSearchBoxByType).exists()).toBe(true);
- });
-
- describe('when assign-self is emitted from IssuableAssignees', () => {
- const currentUser = { username: 'self', name: '', id: '' };
-
- beforeEach(() => {
- window.gon = { current_username: currentUser.username };
-
- dispatchSpy.mockResolvedValue([currentUser]);
- createComponent();
-
- wrapper.find(IssuableAssignees).vm.$emit('assign-self');
- });
-
- it('calls setAssignees with currentUser', () => {
- expect(store.dispatch).toHaveBeenCalledWith('setAssignees', currentUser.username);
- });
-
- it('adds the user to the selected list', async () => {
- expect(findByText(currentUser.username).exists()).toBe(true);
- });
- });
-
- describe('when setting an assignee', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('passes loading state from Vuex to BoardEditableItem', async () => {
- store.state.isSettingAssignees = true;
-
- await wrapper.vm.$nextTick();
-
- expect(wrapper.find(BoardEditableItem).props('loading')).toBe(true);
- });
- });
-});
diff --git a/spec/frontend/boards/components/board_card_layout_deprecated_spec.js b/spec/frontend/boards/components/board_card_layout_deprecated_spec.js
new file mode 100644
index 00000000000..426c5289ba6
--- /dev/null
+++ b/spec/frontend/boards/components/board_card_layout_deprecated_spec.js
@@ -0,0 +1,158 @@
+/* global List */
+/* global ListLabel */
+
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+
+import MockAdapter from 'axios-mock-adapter';
+import Vuex from 'vuex';
+import waitForPromises from 'helpers/wait_for_promises';
+
+import '~/boards/models/label';
+import '~/boards/models/assignee';
+import '~/boards/models/list';
+import BoardCardLayout from '~/boards/components/board_card_layout_deprecated.vue';
+import issueCardInner from '~/boards/components/issue_card_inner.vue';
+import { ISSUABLE } from '~/boards/constants';
+import boardsVuexStore from '~/boards/stores';
+import boardsStore from '~/boards/stores/boards_store';
+import axios from '~/lib/utils/axios_utils';
+import { listObj, boardsMockInterceptor, setMockEndpoints } from '../mock_data';
+
+describe('Board card layout', () => {
+ let wrapper;
+ let mock;
+ let list;
+ let store;
+
+ const localVue = createLocalVue();
+ localVue.use(Vuex);
+
+ const createStore = ({ getters = {}, actions = {} } = {}) => {
+ store = new Vuex.Store({
+ ...boardsVuexStore,
+ actions,
+ getters,
+ });
+ };
+
+ // this particular mount component needs to be used after the root beforeEach because it depends on list being initialized
+ const mountComponent = ({ propsData = {}, provide = {} } = {}) => {
+ wrapper = shallowMount(BoardCardLayout, {
+ localVue,
+ stubs: {
+ issueCardInner,
+ },
+ store,
+ propsData: {
+ list,
+ issue: list.issues[0],
+ disabled: false,
+ index: 0,
+ ...propsData,
+ },
+ provide: {
+ groupId: null,
+ rootPath: '/',
+ scopedLabelsAvailable: false,
+ ...provide,
+ },
+ });
+ };
+
+ const setupData = () => {
+ list = new List(listObj);
+ boardsStore.create();
+ boardsStore.detail.issue = {};
+ const label1 = new ListLabel({
+ id: 3,
+ title: 'testing 123',
+ color: '#000cff',
+ text_color: 'white',
+ description: 'test',
+ });
+ return waitForPromises().then(() => {
+ list.issues[0].labels.push(label1);
+ });
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mock.onAny().reply(boardsMockInterceptor);
+ setMockEndpoints();
+ return setupData();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ list = null;
+ mock.restore();
+ });
+
+ describe('mouse events', () => {
+ it('sets showDetail to true on mousedown', async () => {
+ createStore();
+ mountComponent();
+
+ wrapper.trigger('mousedown');
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.showDetail).toBe(true);
+ });
+
+ it('sets showDetail to false on mousemove', async () => {
+ createStore();
+ mountComponent();
+ wrapper.trigger('mousedown');
+ await wrapper.vm.$nextTick();
+ expect(wrapper.vm.showDetail).toBe(true);
+ wrapper.trigger('mousemove');
+ await wrapper.vm.$nextTick();
+ expect(wrapper.vm.showDetail).toBe(false);
+ });
+
+ it("calls 'setActiveId' when 'graphqlBoardLists' feature flag is turned on", async () => {
+ const setActiveId = jest.fn();
+ createStore({
+ actions: {
+ setActiveId,
+ },
+ });
+ mountComponent({
+ provide: {
+ glFeatures: { graphqlBoardLists: true },
+ },
+ });
+
+ wrapper.trigger('mouseup');
+ await wrapper.vm.$nextTick();
+
+ expect(setActiveId).toHaveBeenCalledTimes(1);
+ expect(setActiveId).toHaveBeenCalledWith(expect.any(Object), {
+ id: list.issues[0].id,
+ sidebarType: ISSUABLE,
+ });
+ });
+
+ it("calls 'setActiveId' when epic swimlanes is active", async () => {
+ const setActiveId = jest.fn();
+ const isSwimlanesOn = () => true;
+ createStore({
+ getters: { isSwimlanesOn },
+ actions: {
+ setActiveId,
+ },
+ });
+ mountComponent();
+
+ wrapper.trigger('mouseup');
+ await wrapper.vm.$nextTick();
+
+ expect(setActiveId).toHaveBeenCalledTimes(1);
+ expect(setActiveId).toHaveBeenCalledWith(expect.any(Object), {
+ id: list.issues[0].id,
+ sidebarType: ISSUABLE,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/boards/components/board_card_layout_spec.js b/spec/frontend/boards/components/board_card_layout_spec.js
index d8633871e8d..3fa8714807c 100644
--- a/spec/frontend/boards/components/board_card_layout_spec.js
+++ b/spec/frontend/boards/components/board_card_layout_spec.js
@@ -1,28 +1,14 @@
-/* global List */
-/* global ListLabel */
-
-import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
+import Vuex from 'vuex';
-import MockAdapter from 'axios-mock-adapter';
-import waitForPromises from 'helpers/wait_for_promises';
-import axios from '~/lib/utils/axios_utils';
-
-import '~/boards/models/label';
-import '~/boards/models/assignee';
-import '~/boards/models/list';
-import boardsVuexStore from '~/boards/stores';
-import boardsStore from '~/boards/stores/boards_store';
import BoardCardLayout from '~/boards/components/board_card_layout.vue';
-import issueCardInner from '~/boards/components/issue_card_inner.vue';
-import { listObj, boardsMockInterceptor, setMockEndpoints } from '../mock_data';
-
+import IssueCardInner from '~/boards/components/issue_card_inner.vue';
import { ISSUABLE } from '~/boards/constants';
+import defaultState from '~/boards/stores/state';
+import { mockLabelList, mockIssue } from '../mock_data';
describe('Board card layout', () => {
let wrapper;
- let mock;
- let list;
let store;
const localVue = createLocalVue();
@@ -30,7 +16,7 @@ describe('Board card layout', () => {
const createStore = ({ getters = {}, actions = {} } = {}) => {
store = new Vuex.Store({
- ...boardsVuexStore,
+ state: defaultState,
actions,
getters,
});
@@ -41,12 +27,12 @@ describe('Board card layout', () => {
wrapper = shallowMount(BoardCardLayout, {
localVue,
stubs: {
- issueCardInner,
+ IssueCardInner,
},
store,
propsData: {
- list,
- issue: list.issues[0],
+ list: mockLabelList,
+ issue: mockIssue,
disabled: false,
index: 0,
...propsData,
@@ -60,34 +46,9 @@ describe('Board card layout', () => {
});
};
- const setupData = () => {
- list = new List(listObj);
- boardsStore.create();
- boardsStore.detail.issue = {};
- const label1 = new ListLabel({
- id: 3,
- title: 'testing 123',
- color: '#000cff',
- text_color: 'white',
- description: 'test',
- });
- return waitForPromises().then(() => {
- list.issues[0].labels.push(label1);
- });
- };
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- mock.onAny().reply(boardsMockInterceptor);
- setMockEndpoints();
- return setupData();
- });
-
afterEach(() => {
wrapper.destroy();
wrapper = null;
- list = null;
- mock.restore();
});
describe('mouse events', () => {
@@ -112,25 +73,21 @@ describe('Board card layout', () => {
expect(wrapper.vm.showDetail).toBe(false);
});
- it("calls 'setActiveId' when 'graphqlBoardLists' feature flag is turned on", async () => {
+ it("calls 'setActiveId'", async () => {
const setActiveId = jest.fn();
createStore({
actions: {
setActiveId,
},
});
- mountComponent({
- provide: {
- glFeatures: { graphqlBoardLists: true },
- },
- });
+ mountComponent();
wrapper.trigger('mouseup');
await wrapper.vm.$nextTick();
expect(setActiveId).toHaveBeenCalledTimes(1);
expect(setActiveId).toHaveBeenCalledWith(expect.any(Object), {
- id: list.issues[0].id,
+ id: mockIssue.id,
sidebarType: ISSUABLE,
});
});
@@ -151,7 +108,7 @@ describe('Board card layout', () => {
expect(setActiveId).toHaveBeenCalledTimes(1);
expect(setActiveId).toHaveBeenCalledWith(expect.any(Object), {
- id: list.issues[0].id,
+ id: mockIssue.id,
sidebarType: ISSUABLE,
});
});
diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js
index 1084009caad..5f26ae1bb3b 100644
--- a/spec/frontend/boards/components/board_card_spec.js
+++ b/spec/frontend/boards/components/board_card_spec.js
@@ -6,17 +6,17 @@ import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
+import BoardCard from '~/boards/components/board_card.vue';
+import issueCardInner from '~/boards/components/issue_card_inner.vue';
+import eventHub from '~/boards/eventhub';
+import store from '~/boards/stores';
+import boardsStore from '~/boards/stores/boards_store';
import axios from '~/lib/utils/axios_utils';
-import eventHub from '~/boards/eventhub';
import sidebarEventHub from '~/sidebar/event_hub';
import '~/boards/models/label';
import '~/boards/models/assignee';
import '~/boards/models/list';
-import store from '~/boards/stores';
-import boardsStore from '~/boards/stores/boards_store';
-import BoardCard from '~/boards/components/board_card.vue';
-import issueCardInner from '~/boards/components/issue_card_inner.vue';
import userAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import { listObj, boardsMockInterceptor, setMockEndpoints } from '../mock_data';
diff --git a/spec/frontend/boards/components/board_column_deprecated_spec.js b/spec/frontend/boards/components/board_column_deprecated_spec.js
index a703caca4eb..e6d65e48c3f 100644
--- a/spec/frontend/boards/components/board_column_deprecated_spec.js
+++ b/spec/frontend/boards/components/board_column_deprecated_spec.js
@@ -1,12 +1,12 @@
-import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
+import Vue, { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
import { listObj } from 'jest/boards/mock_data';
import Board from '~/boards/components/board_column_deprecated.vue';
-import List from '~/boards/models/list';
import { ListType } from '~/boards/constants';
+import List from '~/boards/models/list';
import axios from '~/lib/utils/axios_utils';
describe('Board Column Component', () => {
@@ -30,6 +30,7 @@ describe('Board Column Component', () => {
const createComponent = ({
listType = ListType.backlog,
collapsed = false,
+ highlighted = false,
withLocalStorage = true,
} = {}) => {
const boardId = '1';
@@ -37,6 +38,7 @@ describe('Board Column Component', () => {
const listMock = {
...listObj,
list_type: listType,
+ highlighted,
collapsed,
};
@@ -91,4 +93,14 @@ describe('Board Column Component', () => {
expect(isCollapsed()).toBe(true);
});
});
+
+ describe('highlighting', () => {
+ it('scrolls to column when highlighted', async () => {
+ createComponent({ highlighted: true });
+
+ await nextTick();
+
+ expect(wrapper.element.scrollIntoView).toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/boards/components/board_column_spec.js b/spec/frontend/boards/components/board_column_spec.js
index 1dcdad2b492..4e523d636cd 100644
--- a/spec/frontend/boards/components/board_column_spec.js
+++ b/spec/frontend/boards/components/board_column_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { listObj } from 'jest/boards/mock_data';
import BoardColumn from '~/boards/components/board_column.vue';
@@ -66,4 +67,16 @@ describe('Board Column Component', () => {
expect(isCollapsed()).toBe(true);
});
});
+
+ describe('highlighting', () => {
+ it('scrolls to column when highlighted', async () => {
+ createComponent();
+
+ store.state.highlightedLists.push(listObj.id);
+
+ await nextTick();
+
+ expect(wrapper.element.scrollIntoView).toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/boards/components/board_configuration_options_spec.js b/spec/frontend/boards/components/board_configuration_options_spec.js
index d9614c254e2..6f0971a9458 100644
--- a/spec/frontend/boards/components/board_configuration_options_spec.js
+++ b/spec/frontend/boards/components/board_configuration_options_spec.js
@@ -7,6 +7,7 @@ describe('BoardConfigurationOptions', () => {
const defaultProps = {
hideBacklogList: false,
hideClosedList: false,
+ readonly: false,
};
const createComponent = (props = {}) => {
@@ -61,4 +62,18 @@ describe('BoardConfigurationOptions', () => {
expect(wrapper.emitted('update:hideClosedList')).toEqual([[true]]);
});
+
+ it('renders checkboxes disabled when user does not have edit rights', () => {
+ createComponent({ readonly: true });
+
+ expect(closedListCheckbox().attributes('disabled')).toBe('true');
+ expect(backlogListCheckbox().attributes('disabled')).toBe('true');
+ });
+
+ it('renders checkboxes enabled when user has edit rights', () => {
+ createComponent();
+
+ expect(closedListCheckbox().attributes('disabled')).toBeUndefined();
+ expect(backlogListCheckbox().attributes('disabled')).toBeUndefined();
+ });
});
diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js
index 98be02d7dbf..159b67ccc67 100644
--- a/spec/frontend/boards/components/board_content_spec.js
+++ b/spec/frontend/boards/components/board_content_spec.js
@@ -1,12 +1,12 @@
-import Vuex from 'vuex';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlAlert } from '@gitlab/ui';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
import Draggable from 'vuedraggable';
+import Vuex from 'vuex';
import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue';
import getters from 'ee_else_ce/boards/stores/getters';
import BoardColumnDeprecated from '~/boards/components/board_column_deprecated.vue';
-import { mockLists, mockListsWithModel } from '../mock_data';
import BoardContent from '~/boards/components/board_content.vue';
+import { mockLists, mockListsWithModel } from '../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
diff --git a/spec/frontend/boards/components/board_form_spec.js b/spec/frontend/boards/components/board_form_spec.js
index c34987a55de..858efea99ad 100644
--- a/spec/frontend/boards/components/board_form_spec.js
+++ b/spec/frontend/boards/components/board_form_spec.js
@@ -1,16 +1,15 @@
+import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-
import { TEST_HOST } from 'helpers/test_constants';
-import { GlModal } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
-import { visitUrl } from '~/lib/utils/url_utility';
-import boardsStore from '~/boards/stores/boards_store';
import BoardForm from '~/boards/components/board_form.vue';
-import updateBoardMutation from '~/boards/graphql/board_update.mutation.graphql';
+import { formType } from '~/boards/constants';
import createBoardMutation from '~/boards/graphql/board_create.mutation.graphql';
import destroyBoardMutation from '~/boards/graphql/board_destroy.mutation.graphql';
+import updateBoardMutation from '~/boards/graphql/board_update.mutation.graphql';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn().mockName('visitUrlMock'),
@@ -35,6 +34,7 @@ const defaultProps = {
labelsPath: `${TEST_HOST}/labels/path`,
labelsWebUrl: `${TEST_HOST}/-/labels`,
currentBoard,
+ currentPage: '',
};
describe('BoardForm', () => {
@@ -75,14 +75,12 @@ describe('BoardForm', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
- boardsStore.state.currentPage = null;
mutate = null;
});
describe('when user can not admin the board', () => {
beforeEach(() => {
- boardsStore.state.currentPage = 'new';
- createComponent();
+ createComponent({ currentPage: formType.new });
});
it('hides modal footer when user is not a board admin', () => {
@@ -100,8 +98,7 @@ describe('BoardForm', () => {
describe('when user can admin the board', () => {
beforeEach(() => {
- boardsStore.state.currentPage = 'new';
- createComponent({ canAdminBoard: true });
+ createComponent({ canAdminBoard: true, currentPage: formType.new });
});
it('shows modal footer when user is a board admin', () => {
@@ -118,13 +115,9 @@ describe('BoardForm', () => {
});
describe('when creating a new board', () => {
- beforeEach(() => {
- boardsStore.state.currentPage = 'new';
- });
-
describe('on non-scoped-board', () => {
beforeEach(() => {
- createComponent({ canAdminBoard: true });
+ createComponent({ canAdminBoard: true, currentPage: formType.new });
});
it('clears the form', () => {
@@ -165,7 +158,7 @@ describe('BoardForm', () => {
});
it('does not call API if board name is empty', async () => {
- createComponent({ canAdminBoard: true });
+ createComponent({ canAdminBoard: true, currentPage: formType.new });
findInput().trigger('keyup.enter', { metaKey: true });
await waitForPromises();
@@ -174,7 +167,7 @@ describe('BoardForm', () => {
});
it('calls a correct GraphQL mutation and redirects to correct page from existing board', async () => {
- createComponent({ canAdminBoard: true });
+ createComponent({ canAdminBoard: true, currentPage: formType.new });
fillForm();
await waitForPromises();
@@ -194,7 +187,7 @@ describe('BoardForm', () => {
it('shows an error flash if GraphQL mutation fails', async () => {
mutate = jest.fn().mockRejectedValue('Houston, we have a problem');
- createComponent({ canAdminBoard: true });
+ createComponent({ canAdminBoard: true, currentPage: formType.new });
fillForm();
await waitForPromises();
@@ -209,13 +202,9 @@ describe('BoardForm', () => {
});
describe('when editing a board', () => {
- beforeEach(() => {
- boardsStore.state.currentPage = 'edit';
- });
-
describe('on non-scoped-board', () => {
beforeEach(() => {
- createComponent({ canAdminBoard: true });
+ createComponent({ canAdminBoard: true, currentPage: formType.edit });
});
it('clears the form', () => {
@@ -247,7 +236,7 @@ describe('BoardForm', () => {
},
});
window.location = new URL('https://test/boards/1');
- createComponent({ canAdminBoard: true });
+ createComponent({ canAdminBoard: true, currentPage: formType.edit });
findInput().trigger('keyup.enter', { metaKey: true });
@@ -273,7 +262,7 @@ describe('BoardForm', () => {
},
});
window.location = new URL('https://test/boards/1?group_by=epic');
- createComponent({ canAdminBoard: true });
+ createComponent({ canAdminBoard: true, currentPage: formType.edit });
findInput().trigger('keyup.enter', { metaKey: true });
@@ -294,7 +283,7 @@ describe('BoardForm', () => {
it('shows an error flash if GraphQL mutation fails', async () => {
mutate = jest.fn().mockRejectedValue('Houston, we have a problem');
- createComponent({ canAdminBoard: true });
+ createComponent({ canAdminBoard: true, currentPage: formType.edit });
findInput().trigger('keyup.enter', { metaKey: true });
await waitForPromises();
@@ -308,24 +297,20 @@ describe('BoardForm', () => {
});
describe('when deleting a board', () => {
- beforeEach(() => {
- boardsStore.state.currentPage = 'delete';
- });
-
it('passes correct primary action text and variant', () => {
- createComponent({ canAdminBoard: true });
+ createComponent({ canAdminBoard: true, currentPage: formType.delete });
expect(findModalActionPrimary().text).toBe('Delete');
expect(findModalActionPrimary().attributes[0].variant).toBe('danger');
});
it('renders delete confirmation message', () => {
- createComponent({ canAdminBoard: true });
+ createComponent({ canAdminBoard: true, currentPage: formType.delete });
expect(findDeleteConfirmation().exists()).toBe(true);
});
it('calls a correct GraphQL mutation and redirects to correct page after deleting board', async () => {
mutate = jest.fn().mockResolvedValue({});
- createComponent({ canAdminBoard: true });
+ createComponent({ canAdminBoard: true, currentPage: formType.delete });
findModal().vm.$emit('primary');
await waitForPromises();
@@ -343,7 +328,7 @@ describe('BoardForm', () => {
it('shows an error flash if GraphQL mutation fails', async () => {
mutate = jest.fn().mockRejectedValue('Houston, we have a problem');
- createComponent({ canAdminBoard: true });
+ createComponent({ canAdminBoard: true, currentPage: formType.delete });
findModal().vm.$emit('primary');
await waitForPromises();
diff --git a/spec/frontend/boards/components/board_list_header_deprecated_spec.js b/spec/frontend/boards/components/board_list_header_deprecated_spec.js
index 6207724e6a9..fdc7cd2b1d4 100644
--- a/spec/frontend/boards/components/board_list_header_deprecated_spec.js
+++ b/spec/frontend/boards/components/board_list_header_deprecated_spec.js
@@ -1,12 +1,12 @@
-import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
+import Vue from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
import { listObj } from 'jest/boards/mock_data';
import BoardListHeader from '~/boards/components/board_list_header_deprecated.vue';
-import List from '~/boards/models/list';
import { ListType } from '~/boards/constants';
+import List from '~/boards/models/list';
import axios from '~/lib/utils/axios_utils';
describe('Board List Header Component', () => {
@@ -74,7 +74,13 @@ describe('Board List Header Component', () => {
describe('Add issue button', () => {
const hasNoAddButton = [ListType.closed];
- const hasAddButton = [ListType.backlog, ListType.label, ListType.milestone, ListType.assignee];
+ const hasAddButton = [
+ ListType.backlog,
+ ListType.label,
+ ListType.milestone,
+ ListType.iteration,
+ ListType.assignee,
+ ];
it.each(hasNoAddButton)('does not render when List Type is `%s`', (listType) => {
createComponent({ listType });
diff --git a/spec/frontend/boards/components/board_list_header_spec.js b/spec/frontend/boards/components/board_list_header_spec.js
index 357d05ced02..f30e3792435 100644
--- a/spec/frontend/boards/components/board_list_header_spec.js
+++ b/spec/frontend/boards/components/board_list_header_spec.js
@@ -1,5 +1,5 @@
-import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
import { mockLabelList } from 'jest/boards/mock_data';
import BoardListHeader from '~/boards/components/board_list_header.vue';
@@ -78,7 +78,13 @@ describe('Board List Header Component', () => {
describe('Add issue button', () => {
const hasNoAddButton = [ListType.closed];
- const hasAddButton = [ListType.backlog, ListType.label, ListType.milestone, ListType.assignee];
+ const hasAddButton = [
+ ListType.backlog,
+ ListType.label,
+ ListType.milestone,
+ ListType.iteration,
+ ListType.assignee,
+ ];
it.each(hasNoAddButton)('does not render when List Type is `%s`', (listType) => {
createComponent({ listType });
@@ -167,7 +173,7 @@ describe('Board List Header Component', () => {
describe('user can drag', () => {
const cannotDragList = [ListType.backlog, ListType.closed];
- const canDragList = [ListType.label, ListType.milestone, ListType.assignee];
+ const canDragList = [ListType.label, ListType.milestone, ListType.iteration, ListType.assignee];
it.each(cannotDragList)(
'does not have user-can-drag-class so user cannot drag list',
diff --git a/spec/frontend/boards/components/board_new_issue_spec.js b/spec/frontend/boards/components/board_new_issue_spec.js
index 5a01221a5be..ce8c95527e9 100644
--- a/spec/frontend/boards/components/board_new_issue_spec.js
+++ b/spec/frontend/boards/components/board_new_issue_spec.js
@@ -1,5 +1,5 @@
-import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
import BoardNewIssue from '~/boards/components/board_new_issue.vue';
import '~/boards/models/list';
diff --git a/spec/frontend/boards/components/board_settings_sidebar_spec.js b/spec/frontend/boards/components/board_settings_sidebar_spec.js
index 12c9431f2d4..52b4d71f7b9 100644
--- a/spec/frontend/boards/components/board_settings_sidebar_spec.js
+++ b/spec/frontend/boards/components/board_settings_sidebar_spec.js
@@ -1,14 +1,14 @@
import '~/boards/models/list';
-import MockAdapter from 'axios-mock-adapter';
+import { GlDrawer, GlLabel } from '@gitlab/ui';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
import Vuex from 'vuex';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import { GlDrawer, GlLabel } from '@gitlab/ui';
import BoardSettingsSidebar from '~/boards/components/board_settings_sidebar.vue';
-import boardsStore from '~/boards/stores/boards_store';
+import { inactiveId, LIST } from '~/boards/constants';
import { createStore } from '~/boards/stores';
+import boardsStore from '~/boards/stores/boards_store';
import sidebarEventHub from '~/sidebar/event_hub';
-import { inactiveId, LIST } from '~/boards/constants';
const localVue = createLocalVue();
diff --git a/spec/frontend/boards/components/boards_selector_deprecated_spec.js b/spec/frontend/boards/components/boards_selector_deprecated_spec.js
new file mode 100644
index 00000000000..cc078861d75
--- /dev/null
+++ b/spec/frontend/boards/components/boards_selector_deprecated_spec.js
@@ -0,0 +1,214 @@
+import { GlDropdown, GlLoadingIcon, GlDropdownSectionHeader } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import { TEST_HOST } from 'spec/test_constants';
+import BoardsSelector from '~/boards/components/boards_selector_deprecated.vue';
+import boardsStore from '~/boards/stores/boards_store';
+
+const throttleDuration = 1;
+
+function boardGenerator(n) {
+ return new Array(n).fill().map((board, index) => {
+ const id = `${index}`;
+ const name = `board${id}`;
+
+ return {
+ id,
+ name,
+ };
+ });
+}
+
+describe('BoardsSelector', () => {
+ let wrapper;
+ let allBoardsResponse;
+ let recentBoardsResponse;
+ const boards = boardGenerator(20);
+ const recentBoards = boardGenerator(5);
+
+ const fillSearchBox = (filterTerm) => {
+ const searchBox = wrapper.find({ ref: 'searchBox' });
+ const searchBoxInput = searchBox.find('input');
+ searchBoxInput.setValue(filterTerm);
+ searchBoxInput.trigger('input');
+ };
+
+ const getDropdownItems = () => wrapper.findAll('.js-dropdown-item');
+ const getDropdownHeaders = () => wrapper.findAll(GlDropdownSectionHeader);
+ const getLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const findDropdown = () => wrapper.find(GlDropdown);
+
+ beforeEach(() => {
+ const $apollo = {
+ queries: {
+ boards: {
+ loading: false,
+ },
+ },
+ };
+
+ boardsStore.setEndpoints({
+ boardsEndpoint: '',
+ recentBoardsEndpoint: '',
+ listsEndpoint: '',
+ bulkUpdatePath: '',
+ boardId: '',
+ });
+
+ allBoardsResponse = Promise.resolve({
+ data: {
+ group: {
+ boards: {
+ edges: boards.map((board) => ({ node: board })),
+ },
+ },
+ },
+ });
+ recentBoardsResponse = Promise.resolve({
+ data: recentBoards,
+ });
+
+ boardsStore.allBoards = jest.fn(() => allBoardsResponse);
+ boardsStore.recentBoards = jest.fn(() => recentBoardsResponse);
+
+ wrapper = mount(BoardsSelector, {
+ propsData: {
+ throttleDuration,
+ currentBoard: {
+ id: 1,
+ name: 'Development',
+ milestone_id: null,
+ weight: null,
+ assignee_id: null,
+ labels: [],
+ },
+ boardBaseUrl: `${TEST_HOST}/board/base/url`,
+ hasMissingBoards: false,
+ canAdminBoard: true,
+ multipleIssueBoardsAvailable: true,
+ labelsPath: `${TEST_HOST}/labels/path`,
+ labelsWebUrl: `${TEST_HOST}/labels`,
+ projectId: 42,
+ groupId: 19,
+ scopedIssueBoardFeatureEnabled: true,
+ weights: [],
+ },
+ mocks: { $apollo },
+ attachTo: document.body,
+ });
+
+ wrapper.vm.$apollo.addSmartQuery = jest.fn((_, options) => {
+ wrapper.setData({
+ [options.loadingKey]: true,
+ });
+ });
+
+ // Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
+ findDropdown().vm.$emit('show');
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('loading', () => {
+ // we are testing loading state, so don't resolve responses until after the tests
+ afterEach(() => {
+ return Promise.all([allBoardsResponse, recentBoardsResponse]).then(() => nextTick());
+ });
+
+ it('shows loading spinner', () => {
+ expect(getDropdownHeaders()).toHaveLength(0);
+ expect(getDropdownItems()).toHaveLength(0);
+ expect(getLoadingIcon().exists()).toBe(true);
+ });
+ });
+
+ describe('loaded', () => {
+ beforeEach(async () => {
+ await wrapper.setData({
+ loadingBoards: false,
+ });
+ return Promise.all([allBoardsResponse, recentBoardsResponse]).then(() => nextTick());
+ });
+
+ it('hides loading spinner', () => {
+ expect(getLoadingIcon().exists()).toBe(false);
+ });
+
+ describe('filtering', () => {
+ beforeEach(() => {
+ wrapper.setData({
+ boards,
+ });
+
+ return nextTick();
+ });
+
+ it('shows all boards without filtering', () => {
+ expect(getDropdownItems()).toHaveLength(boards.length + recentBoards.length);
+ });
+
+ it('shows only matching boards when filtering', () => {
+ const filterTerm = 'board1';
+ const expectedCount = boards.filter((board) => board.name.includes(filterTerm)).length;
+
+ fillSearchBox(filterTerm);
+
+ return nextTick().then(() => {
+ expect(getDropdownItems()).toHaveLength(expectedCount);
+ });
+ });
+
+ it('shows message if there are no matching boards', () => {
+ fillSearchBox('does not exist');
+
+ return nextTick().then(() => {
+ expect(getDropdownItems()).toHaveLength(0);
+ expect(wrapper.text().includes('No matching boards found')).toBe(true);
+ });
+ });
+ });
+
+ describe('recent boards section', () => {
+ it('shows only when boards are greater than 10', () => {
+ wrapper.setData({
+ boards,
+ });
+
+ return nextTick().then(() => {
+ expect(getDropdownHeaders()).toHaveLength(2);
+ });
+ });
+
+ it('does not show when boards are less than 10', () => {
+ wrapper.setData({
+ boards: boards.slice(0, 5),
+ });
+
+ return nextTick().then(() => {
+ expect(getDropdownHeaders()).toHaveLength(0);
+ });
+ });
+
+ it('does not show when recentBoards api returns empty array', () => {
+ wrapper.setData({
+ recentBoards: [],
+ });
+
+ return nextTick().then(() => {
+ expect(getDropdownHeaders()).toHaveLength(0);
+ });
+ });
+
+ it('does not show when search is active', () => {
+ fillSearchBox('Random string');
+
+ return nextTick().then(() => {
+ expect(getDropdownHeaders()).toHaveLength(0);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js
index 81575bf486a..bf317b51e83 100644
--- a/spec/frontend/boards/components/boards_selector_spec.js
+++ b/spec/frontend/boards/components/boards_selector_spec.js
@@ -1,9 +1,10 @@
-import { nextTick } from 'vue';
-import { mount } from '@vue/test-utils';
import { GlDropdown, GlLoadingIcon, GlDropdownSectionHeader } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import { TEST_HOST } from 'spec/test_constants';
import BoardsSelector from '~/boards/components/boards_selector.vue';
-import boardsStore from '~/boards/stores/boards_store';
+import axios from '~/lib/utils/axios_utils';
const throttleDuration = 1;
@@ -23,6 +24,7 @@ describe('BoardsSelector', () => {
let wrapper;
let allBoardsResponse;
let recentBoardsResponse;
+ let mock;
const boards = boardGenerator(20);
const recentBoards = boardGenerator(5);
@@ -39,6 +41,7 @@ describe('BoardsSelector', () => {
const findDropdown = () => wrapper.find(GlDropdown);
beforeEach(() => {
+ mock = new MockAdapter(axios);
const $apollo = {
queries: {
boards: {
@@ -47,14 +50,6 @@ describe('BoardsSelector', () => {
},
};
- boardsStore.setEndpoints({
- boardsEndpoint: '',
- recentBoardsEndpoint: '',
- listsEndpoint: '',
- bulkUpdatePath: '',
- boardId: '',
- });
-
allBoardsResponse = Promise.resolve({
data: {
group: {
@@ -68,9 +63,6 @@ describe('BoardsSelector', () => {
data: recentBoards,
});
- boardsStore.allBoards = jest.fn(() => allBoardsResponse);
- boardsStore.recentBoards = jest.fn(() => recentBoardsResponse);
-
wrapper = mount(BoardsSelector, {
propsData: {
throttleDuration,
@@ -95,6 +87,10 @@ describe('BoardsSelector', () => {
},
mocks: { $apollo },
attachTo: document.body,
+ provide: {
+ fullPath: '',
+ recentBoardsEndpoint: `${TEST_HOST}/recent`,
+ },
});
wrapper.vm.$apollo.addSmartQuery = jest.fn((_, options) => {
@@ -103,6 +99,8 @@ describe('BoardsSelector', () => {
});
});
+ mock.onGet(`${TEST_HOST}/recent`).replyOnce(200, recentBoards);
+
// Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
findDropdown().vm.$emit('show');
});
@@ -110,6 +108,7 @@ describe('BoardsSelector', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
+ mock.restore();
});
describe('loading', () => {
@@ -133,7 +132,8 @@ describe('BoardsSelector', () => {
return Promise.all([allBoardsResponse, recentBoardsResponse]).then(() => nextTick());
});
- it('hides loading spinner', () => {
+ it('hides loading spinner', async () => {
+ await wrapper.vm.$nextTick();
expect(getLoadingIcon().exists()).toBe(false);
});
diff --git a/spec/frontend/boards/components/issue_time_estimate_spec.js b/spec/frontend/boards/components/issue_time_estimate_spec.js
index 9ac8fae3fcc..2e253d24125 100644
--- a/spec/frontend/boards/components/issue_time_estimate_spec.js
+++ b/spec/frontend/boards/components/issue_time_estimate_spec.js
@@ -1,5 +1,5 @@
-import { config as vueConfig } from 'vue';
import { shallowMount } from '@vue/test-utils';
+import { config as vueConfig } from 'vue';
import IssueTimeEstimate from '~/boards/components/issue_time_estimate.vue';
describe('Issue Time Estimate component', () => {
diff --git a/spec/frontend/boards/components/sidebar/board_editable_item_spec.js b/spec/frontend/boards/components/sidebar/board_editable_item_spec.js
index de414bb929e..12e9a9ba365 100644
--- a/spec/frontend/boards/components/sidebar/board_editable_item_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_editable_item_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import BoardSidebarItem from '~/boards/components/sidebar/board_editable_item.vue';
describe('boards sidebar remove issue', () => {
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_due_date_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_due_date_spec.js
index b034c8cb11d..7838b5a0b2f 100644
--- a/spec/frontend/boards/components/sidebar/board_sidebar_due_date_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_due_date_spec.js
@@ -1,7 +1,7 @@
-import { shallowMount } from '@vue/test-utils';
import { GlDatepicker } from '@gitlab/ui';
-import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue';
+import { shallowMount } from '@vue/test-utils';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
+import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue';
import { createStore } from '~/boards/stores';
import createFlash from '~/flash';
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_issue_title_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_issue_title_spec.js
index 86895c648a4..bc7df1c76c6 100644
--- a/spec/frontend/boards/components/sidebar/board_sidebar_issue_title_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_issue_title_spec.js
@@ -1,9 +1,9 @@
-import { shallowMount } from '@vue/test-utils';
import { GlAlert, GlFormInput, GlForm } from '@gitlab/ui';
-import BoardSidebarIssueTitle from '~/boards/components/sidebar/board_sidebar_issue_title.vue';
+import { shallowMount } from '@vue/test-utils';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
-import createFlash from '~/flash';
+import BoardSidebarIssueTitle from '~/boards/components/sidebar/board_sidebar_issue_title.vue';
import { createStore } from '~/boards/stores';
+import createFlash from '~/flash';
const TEST_TITLE = 'New issue title';
const TEST_ISSUE_A = {
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js
index 2342caa9dfd..12b873ba7d8 100644
--- a/spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js
@@ -1,12 +1,12 @@
-import { shallowMount } from '@vue/test-utils';
import { GlLabel } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
import { labels as TEST_LABELS, mockIssue as TEST_ISSUE } from 'jest/boards/mock_data';
-import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
import { createStore } from '~/boards/stores';
import createFlash from '~/flash';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
jest.mock('~/flash');
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_milestone_select_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_milestone_select_spec.js
index 74d88d9f34c..8820ec7ae63 100644
--- a/spec/frontend/boards/components/sidebar/board_sidebar_milestone_select_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_milestone_select_spec.js
@@ -1,8 +1,8 @@
+import { GlLoadingIcon, GlDropdown } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { GlLoadingIcon } from '@gitlab/ui';
import { mockMilestone as TEST_MILESTONE } from 'jest/boards/mock_data';
-import BoardSidebarMilestoneSelect from '~/boards/components/sidebar/board_sidebar_milestone_select.vue';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
+import BoardSidebarMilestoneSelect from '~/boards/components/sidebar/board_sidebar_milestone_select.vue';
import { createStore } from '~/boards/stores';
import createFlash from '~/flash';
@@ -20,7 +20,7 @@ describe('~/boards/components/sidebar/board_sidebar_milestone_select.vue', () =>
wrapper = null;
});
- const createWrapper = ({ milestone = null } = {}) => {
+ const createWrapper = ({ milestone = null, loading = false } = {}) => {
store = createStore();
store.state.issues = { [TEST_ISSUE.id]: { ...TEST_ISSUE, milestone } };
store.state.activeId = TEST_ISSUE.id;
@@ -38,7 +38,7 @@ describe('~/boards/components/sidebar/board_sidebar_milestone_select.vue', () =>
},
mocks: {
$apollo: {
- loading: false,
+ loading,
},
},
});
@@ -46,10 +46,42 @@ describe('~/boards/components/sidebar/board_sidebar_milestone_select.vue', () =>
const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]');
const findLoader = () => wrapper.find(GlLoadingIcon);
+ const findDropdown = () => wrapper.find(GlDropdown);
+ const findBoardEditableItem = () => wrapper.find(BoardEditableItem);
const findDropdownItem = () => wrapper.find('[data-testid="milestone-item"]');
const findUnsetMilestoneItem = () => wrapper.find('[data-testid="no-milestone-item"]');
const findNoMilestonesFoundItem = () => wrapper.find('[data-testid="no-milestones-found"]');
+ describe('when not editing', () => {
+ it('opens the milestone dropdown on clicking edit', async () => {
+ createWrapper();
+ wrapper.vm.$refs.dropdown.show = jest.fn();
+
+ await findBoardEditableItem().vm.$emit('open');
+
+ expect(wrapper.vm.$refs.dropdown.show).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('when editing', () => {
+ beforeEach(() => {
+ createWrapper();
+ jest.spyOn(wrapper.vm.$refs.sidebarItem, 'collapse');
+ });
+
+ it('collapses BoardEditableItem on clicking edit', async () => {
+ await findBoardEditableItem().vm.$emit('close');
+
+ expect(wrapper.vm.$refs.sidebarItem.collapse).toHaveBeenCalledTimes(1);
+ });
+
+ it('collapses BoardEditableItem on hiding dropdown', async () => {
+ await findDropdown().vm.$emit('hide');
+
+ expect(wrapper.vm.$refs.sidebarItem.collapse).toHaveBeenCalledTimes(1);
+ });
+ });
+
it('renders "None" when no milestone is selected', () => {
createWrapper();
@@ -63,12 +95,7 @@ describe('~/boards/components/sidebar/board_sidebar_milestone_select.vue', () =>
});
it('shows loader while Apollo is loading', async () => {
- createWrapper({ milestone: TEST_MILESTONE });
-
- expect(findLoader().exists()).toBe(false);
-
- wrapper.vm.$apollo.loading = true;
- await wrapper.vm.$nextTick();
+ createWrapper({ milestone: TEST_MILESTONE, loading: true });
expect(findLoader().exists()).toBe(true);
});
@@ -76,8 +103,7 @@ describe('~/boards/components/sidebar/board_sidebar_milestone_select.vue', () =>
it('shows message when error or no milestones found', async () => {
createWrapper();
- wrapper.setData({ milestones: [] });
- await wrapper.vm.$nextTick();
+ await wrapper.setData({ milestones: [] });
expect(findNoMilestonesFoundItem().text()).toBe('No milestones found');
});
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js
index b1df0f2d771..3e6b0be0267 100644
--- a/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js
@@ -1,11 +1,11 @@
-import Vuex from 'vuex';
-import { mount, createLocalVue } from '@vue/test-utils';
import { GlToggle, GlLoadingIcon } from '@gitlab/ui';
+import { mount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
import BoardSidebarSubscription from '~/boards/components/sidebar/board_sidebar_subscription.vue';
-import * as types from '~/boards/stores/mutation_types';
import { createStore } from '~/boards/stores';
-import { mockActiveIssue } from '../../mock_data';
+import * as types from '~/boards/stores/mutation_types';
import createFlash from '~/flash';
+import { mockActiveIssue } from '../../mock_data';
jest.mock('~/flash.js');
diff --git a/spec/frontend/boards/components/sidebar/remove_issue_spec.js b/spec/frontend/boards/components/sidebar/remove_issue_spec.js
index 1b7a78e6e58..1f740c10106 100644
--- a/spec/frontend/boards/components/sidebar/remove_issue_spec.js
+++ b/spec/frontend/boards/components/sidebar/remove_issue_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import RemoveIssue from '~/boards/components/sidebar/remove_issue.vue';
diff --git a/spec/frontend/boards/issue_card_deprecated_spec.js b/spec/frontend/boards/issue_card_deprecated_spec.js
index fd7b0edb97e..909be275030 100644
--- a/spec/frontend/boards/issue_card_deprecated_spec.js
+++ b/spec/frontend/boards/issue_card_deprecated_spec.js
@@ -1,14 +1,14 @@
/* global ListAssignee, ListLabel, ListIssue */
+import { GlLabel } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { range } from 'lodash';
import '~/boards/models/label';
import '~/boards/models/assignee';
import '~/boards/models/issue';
import '~/boards/models/list';
-import { GlLabel } from '@gitlab/ui';
import IssueCardInner from '~/boards/components/issue_card_inner_deprecated.vue';
-import { listObj } from './mock_data';
import store from '~/boards/stores';
+import { listObj } from './mock_data';
describe('Issue card component', () => {
const user = new ListAssignee({
diff --git a/spec/frontend/boards/issue_card_inner_spec.js b/spec/frontend/boards/issue_card_inner_spec.js
index f9ad78494af..b9f84fed6b3 100644
--- a/spec/frontend/boards/issue_card_inner_spec.js
+++ b/spec/frontend/boards/issue_card_inner_spec.js
@@ -1,11 +1,11 @@
+import { GlLabel } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { range } from 'lodash';
-import { GlLabel } from '@gitlab/ui';
import IssueCardInner from '~/boards/components/issue_card_inner.vue';
-import { mockLabelList } from './mock_data';
-import defaultStore from '~/boards/stores';
import eventHub from '~/boards/eventhub';
+import defaultStore from '~/boards/stores';
import { updateHistory } from '~/lib/utils/url_utility';
+import { mockLabelList } from './mock_data';
jest.mock('~/lib/utils/url_utility');
jest.mock('~/boards/eventhub');
diff --git a/spec/frontend/boards/issue_spec.js b/spec/frontend/boards/issue_spec.js
index d68e17c06a7..1f354fb04db 100644
--- a/spec/frontend/boards/issue_spec.js
+++ b/spec/frontend/boards/issue_spec.js
@@ -41,7 +41,7 @@ describe('Issue model', () => {
});
expect(issue.labels.length).toBe(1);
- expect(issue.labels[0].color).toBe('red');
+ expect(issue.labels[0].color).toBe('#F0AD4E');
});
it('adds other label with same title', () => {
diff --git a/spec/frontend/boards/list_spec.js b/spec/frontend/boards/list_spec.js
index db01f62c9a6..4d6a82bdff0 100644
--- a/spec/frontend/boards/list_spec.js
+++ b/spec/frontend/boards/list_spec.js
@@ -2,16 +2,15 @@
/* global ListAssignee */
/* global ListIssue */
/* global ListLabel */
-
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
-import axios from '~/lib/utils/axios_utils';
import '~/boards/models/label';
import '~/boards/models/assignee';
import '~/boards/models/issue';
import '~/boards/models/list';
import { ListType } from '~/boards/constants';
import boardsStore from '~/boards/stores/boards_store';
+import axios from '~/lib/utils/axios_utils';
import { listObj, listObjDuplicate, boardsMockInterceptor } from './mock_data';
describe('List model', () => {
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index d5cfb9b7d07..e106b9235d6 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -1,7 +1,7 @@
/* global List */
-import Vue from 'vue';
import { keyBy } from 'lodash';
+import Vue from 'vue';
import '~/boards/models/list';
import boardsStore from '~/boards/stores/boards_store';
@@ -137,7 +137,7 @@ export const rawIssue = {
{
id: 1,
title: 'test',
- color: 'red',
+ color: '#F0AD4E',
description: 'testing',
},
],
@@ -165,7 +165,7 @@ export const mockIssue = {
{
id: 1,
title: 'test',
- color: 'red',
+ color: '#F0AD4E',
description: 'testing',
},
],
diff --git a/spec/frontend/boards/project_select_deprecated_spec.js b/spec/frontend/boards/project_select_deprecated_spec.js
index e4f8f96bd33..9042c4bf9ba 100644
--- a/spec/frontend/boards/project_select_deprecated_spec.js
+++ b/spec/frontend/boards/project_select_deprecated_spec.js
@@ -1,14 +1,13 @@
+import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import axios from 'axios';
import AxiosMockAdapter from 'axios-mock-adapter';
-import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlLoadingIcon } from '@gitlab/ui';
-import httpStatus from '~/lib/utils/http_status';
-import { featureAccessLevel } from '~/pages/projects/shared/permissions/constants';
+import ProjectSelect from '~/boards/components/project_select_deprecated.vue';
import { ListType } from '~/boards/constants';
import eventHub from '~/boards/eventhub';
import { deprecatedCreateFlash as flash } from '~/flash';
-
-import ProjectSelect from '~/boards/components/project_select_deprecated.vue';
+import httpStatus from '~/lib/utils/http_status';
+import { featureAccessLevel } from '~/pages/projects/shared/permissions/constants';
import { listObj, mockRawGroupProjects } from './mock_data';
diff --git a/spec/frontend/boards/project_select_spec.js b/spec/frontend/boards/project_select_spec.js
index 14ddab3542b..aa71952c42b 100644
--- a/spec/frontend/boards/project_select_spec.js
+++ b/spec/frontend/boards/project_select_spec.js
@@ -1,9 +1,8 @@
-import Vuex from 'vuex';
-import { createLocalVue, mount } from '@vue/test-utils';
import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlLoadingIcon } from '@gitlab/ui';
-import defaultState from '~/boards/stores/state';
-
+import { createLocalVue, mount } from '@vue/test-utils';
+import Vuex from 'vuex';
import ProjectSelect from '~/boards/components/project_select.vue';
+import defaultState from '~/boards/stores/state';
import { mockList, mockGroupProjects } from './mock_data';
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index e4209cd5e55..32d0e7ae886 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -1,5 +1,17 @@
import testAction from 'helpers/vuex_action_helper';
import {
+ fullBoardId,
+ formatListIssues,
+ formatBoardLists,
+ formatIssueInput,
+} from '~/boards/boards_util';
+import { inactiveId } from '~/boards/constants';
+import destroyBoardListMutation from '~/boards/graphql/board_list_destroy.mutation.graphql';
+import issueCreateMutation from '~/boards/graphql/issue_create.mutation.graphql';
+import issueMoveListMutation from '~/boards/graphql/issue_move_list.mutation.graphql';
+import actions, { gqlClient } from '~/boards/stores/actions';
+import * as types from '~/boards/stores/mutation_types';
+import {
mockLists,
mockListsById,
mockIssue,
@@ -11,20 +23,6 @@ import {
mockActiveIssue,
mockGroupProjects,
} from '../mock_data';
-import actions, { gqlClient } from '~/boards/stores/actions';
-import * as types from '~/boards/stores/mutation_types';
-import { inactiveId } from '~/boards/constants';
-import issueMoveListMutation from '~/boards/graphql/issue_move_list.mutation.graphql';
-import destroyBoardListMutation from '~/boards/graphql/board_list_destroy.mutation.graphql';
-import issueCreateMutation from '~/boards/graphql/issue_create.mutation.graphql';
-import updateAssignees from '~/vue_shared/components/sidebar/queries/updateAssignees.mutation.graphql';
-import {
- fullBoardId,
- formatListIssues,
- formatBoardLists,
- formatIssueInput,
-} from '~/boards/boards_util';
-import createFlash from '~/flash';
jest.mock('~/flash');
@@ -71,7 +69,7 @@ describe('setFilters', () => {
actions.setFilters,
filters,
state,
- [{ type: types.SET_FILTERS, payload: filters }],
+ [{ type: types.SET_FILTERS, payload: { ...filters, not: {} } }],
[],
done,
);
@@ -186,7 +184,27 @@ describe('fetchLists', () => {
});
describe('createList', () => {
- it('should dispatch addList action when creating backlog list', (done) => {
+ let commit;
+ let dispatch;
+ let getters;
+ let state;
+
+ beforeEach(() => {
+ state = {
+ fullPath: 'gitlab-org',
+ boardId: '1',
+ boardType: 'group',
+ disabled: false,
+ boardLists: [{ type: 'closed' }],
+ };
+ commit = jest.fn();
+ dispatch = jest.fn();
+ getters = {
+ getListByLabelId: jest.fn(),
+ };
+ });
+
+ it('should dispatch addList action when creating backlog list', async () => {
const backlogList = {
id: 'gid://gitlab/List/1',
listType: 'backlog',
@@ -205,25 +223,35 @@ describe('createList', () => {
}),
);
- const state = {
- fullPath: 'gitlab-org',
- boardId: '1',
- boardType: 'group',
- disabled: false,
- boardLists: [{ type: 'closed' }],
+ await actions.createList({ getters, state, commit, dispatch }, { backlog: true });
+
+ expect(dispatch).toHaveBeenCalledWith('addList', backlogList);
+ });
+
+ it('dispatches highlightList after addList has succeeded', async () => {
+ const list = {
+ id: 'gid://gitlab/List/1',
+ listType: 'label',
+ title: 'Open',
+ labelId: '4',
};
- testAction(
- actions.createList,
- { backlog: true },
- state,
- [],
- [{ type: 'addList', payload: backlogList }],
- done,
- );
+ jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
+ data: {
+ boardListCreate: {
+ list,
+ errors: [],
+ },
+ },
+ });
+
+ await actions.createList({ getters, state, commit, dispatch }, { labelId: '4' });
+
+ expect(dispatch).toHaveBeenCalledWith('addList', list);
+ expect(dispatch).toHaveBeenCalledWith('highlightList', list.id);
});
- it('should commit CREATE_LIST_FAILURE mutation when API returns an error', (done) => {
+ it('should commit CREATE_LIST_FAILURE mutation when API returns an error', async () => {
jest.spyOn(gqlClient, 'mutate').mockReturnValue(
Promise.resolve({
data: {
@@ -235,22 +263,49 @@ describe('createList', () => {
}),
);
- const state = {
- fullPath: 'gitlab-org',
- boardId: '1',
- boardType: 'group',
- disabled: false,
- boardLists: [{ type: 'closed' }],
+ await actions.createList({ getters, state, commit, dispatch }, { backlog: true });
+
+ expect(commit).toHaveBeenCalledWith(types.CREATE_LIST_FAILURE);
+ });
+
+ it('highlights list and does not re-query if it already exists', async () => {
+ const existingList = {
+ id: 'gid://gitlab/List/1',
+ listType: 'label',
+ title: 'Some label',
+ position: 1,
};
- testAction(
- actions.createList,
- { backlog: true },
- state,
- [{ type: types.CREATE_LIST_FAILURE }],
- [],
- done,
- );
+ getters = {
+ getListByLabelId: jest.fn().mockReturnValue(existingList),
+ };
+
+ await actions.createList({ getters, state, commit, dispatch }, { backlog: true });
+
+ expect(dispatch).toHaveBeenCalledWith('highlightList', existingList.id);
+ expect(dispatch).toHaveBeenCalledTimes(1);
+ expect(commit).not.toHaveBeenCalled();
+ });
+});
+
+describe('fetchLabels', () => {
+ it('should commit mutation RECEIVE_LABELS_SUCCESS on success', async () => {
+ const queryResponse = {
+ data: {
+ group: {
+ labels: {
+ nodes: labels,
+ },
+ },
+ },
+ };
+ jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
+
+ await testAction({
+ action: actions.fetchLabels,
+ state: { boardType: 'group' },
+ expectedMutations: [{ type: types.RECEIVE_LABELS_SUCCESS, payload: labels }],
+ });
});
});
@@ -669,65 +724,27 @@ describe('moveIssue', () => {
describe('setAssignees', () => {
const node = { username: 'name' };
- const name = 'username';
const projectPath = 'h/h';
const refPath = `${projectPath}#3`;
const iid = '1';
describe('when succeeds', () => {
- beforeEach(() => {
- jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
- data: { issueSetAssignees: { issue: { assignees: { nodes: [{ ...node }] } } } },
- });
- });
-
- it('calls mutate with the correct values', async () => {
- await actions.setAssignees(
- { commit: () => {}, getters: { activeIssue: { iid, referencePath: refPath } } },
- [name],
- );
-
- expect(gqlClient.mutate).toHaveBeenCalledWith({
- mutation: updateAssignees,
- variables: { iid, assigneeUsernames: [name], projectPath },
- });
- });
-
it('calls the correct mutation with the correct values', (done) => {
testAction(
actions.setAssignees,
- {},
+ [node],
{ activeIssue: { iid, referencePath: refPath }, commit: () => {} },
[
- { type: types.SET_ASSIGNEE_LOADING, payload: true },
{
type: 'UPDATE_ISSUE_BY_ID',
payload: { prop: 'assignees', issueId: undefined, value: [node] },
},
- { type: types.SET_ASSIGNEE_LOADING, payload: false },
],
[],
done,
);
});
});
-
- describe('when fails', () => {
- beforeEach(() => {
- jest.spyOn(gqlClient, 'mutate').mockRejectedValue();
- });
-
- it('calls createFlash', async () => {
- await actions.setAssignees({
- commit: () => {},
- getters: { activeIssue: { iid, referencePath: refPath } },
- });
-
- expect(createFlash).toHaveBeenCalledWith({
- message: 'An error occurred while updating assignees.',
- });
- });
- });
});
describe('createNewIssue', () => {
@@ -1201,6 +1218,40 @@ describe('setSelectedProject', () => {
});
});
+describe('toggleBoardItemMultiSelection', () => {
+ const boardItem = mockIssue;
+
+ it('should commit mutation ADD_BOARD_ITEM_TO_SELECTION if item is not on selection state', () => {
+ testAction(
+ actions.toggleBoardItemMultiSelection,
+ boardItem,
+ { selectedBoardItems: [] },
+ [
+ {
+ type: types.ADD_BOARD_ITEM_TO_SELECTION,
+ payload: boardItem,
+ },
+ ],
+ [],
+ );
+ });
+
+ it('should commit mutation REMOVE_BOARD_ITEM_FROM_SELECTION if item is on selection state', () => {
+ testAction(
+ actions.toggleBoardItemMultiSelection,
+ boardItem,
+ { selectedBoardItems: [mockIssue] },
+ [
+ {
+ type: types.REMOVE_BOARD_ITEM_FROM_SELECTION,
+ payload: boardItem,
+ },
+ ],
+ [],
+ );
+ });
+});
+
describe('fetchBacklog', () => {
expectNotImplemented(actions.fetchBacklog);
});
diff --git a/spec/frontend/boards/stores/getters_spec.js b/spec/frontend/boards/stores/getters_spec.js
index 44b41b5667d..d5a19bf613f 100644
--- a/spec/frontend/boards/stores/getters_spec.js
+++ b/spec/frontend/boards/stores/getters_spec.js
@@ -1,5 +1,5 @@
-import getters from '~/boards/stores/getters';
import { inactiveId } from '~/boards/constants';
+import getters from '~/boards/stores/getters';
import {
mockIssue,
mockIssue2,
@@ -62,6 +62,22 @@ describe('Boards - Getters', () => {
});
});
+ describe('groupPathByIssueId', () => {
+ it('returns group path for the active issue', () => {
+ const mockActiveIssue = {
+ referencePath: 'gitlab-org/gitlab-test#1',
+ };
+ expect(getters.groupPathForActiveIssue({}, { activeIssue: mockActiveIssue })).toEqual(
+ 'gitlab-org',
+ );
+ });
+
+ it('returns empty string as group path when active issue is an empty object', () => {
+ const mockActiveIssue = {};
+ expect(getters.groupPathForActiveIssue({}, { activeIssue: mockActiveIssue })).toEqual('');
+ });
+ });
+
describe('projectPathByIssueId', () => {
it('returns project path for the active issue', () => {
const mockActiveIssue = {
@@ -72,7 +88,7 @@ describe('Boards - Getters', () => {
);
});
- it('returns empty string as project when active issue is an empty object', () => {
+ it('returns empty string as project path when active issue is an empty object', () => {
const mockActiveIssue = {};
expect(getters.projectPathForActiveIssue({}, { activeIssue: mockActiveIssue })).toEqual('');
});
diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js
index c5fe0e22c3c..9423f2ed583 100644
--- a/spec/frontend/boards/stores/mutations_spec.js
+++ b/spec/frontend/boards/stores/mutations_spec.js
@@ -1,7 +1,14 @@
-import mutations from '~/boards/stores/mutations';
import * as types from '~/boards/stores/mutation_types';
+import mutations from '~/boards/stores/mutations';
import defaultState from '~/boards/stores/state';
-import { mockLists, rawIssue, mockIssue, mockIssue2, mockGroupProjects } from '../mock_data';
+import {
+ mockLists,
+ rawIssue,
+ mockIssue,
+ mockIssue2,
+ mockGroupProjects,
+ labels,
+} from '../mock_data';
const expectNotImplemented = (action) => {
it('is not implemented', () => {
@@ -99,13 +106,11 @@ describe('Board Store Mutations', () => {
});
});
- describe('RECEIVE_LABELS_FAILURE', () => {
- it('sets error message', () => {
- mutations.RECEIVE_LABELS_FAILURE(state);
+ describe('RECEIVE_LABELS_SUCCESS', () => {
+ it('sets labels on state', () => {
+ mutations.RECEIVE_LABELS_SUCCESS(state, labels);
- expect(state.error).toEqual(
- 'An error occurred while fetching labels. Please reload the page.',
- );
+ expect(state.labels).toEqual(labels);
});
});
@@ -589,4 +594,27 @@ describe('Board Store Mutations', () => {
expect(state.selectedProject).toEqual(mockGroupProjects[0]);
});
});
+
+ describe('ADD_BOARD_ITEM_TO_SELECTION', () => {
+ it('Should add boardItem to selectedBoardItems state', () => {
+ expect(state.selectedBoardItems).toEqual([]);
+
+ mutations[types.ADD_BOARD_ITEM_TO_SELECTION](state, mockIssue);
+
+ expect(state.selectedBoardItems).toEqual([mockIssue]);
+ });
+ });
+
+ describe('REMOVE_BOARD_ITEM_FROM_SELECTION', () => {
+ it('Should remove boardItem to selectedBoardItems state', () => {
+ state = {
+ ...state,
+ selectedBoardItems: [mockIssue],
+ };
+
+ mutations[types.REMOVE_BOARD_ITEM_FROM_SELECTION](state, mockIssue);
+
+ expect(state.selectedBoardItems).toEqual([]);
+ });
+ });
});
diff --git a/spec/frontend/branches/divergence_graph_spec.js b/spec/frontend/branches/divergence_graph_spec.js
index adf39a2216a..be97a1724d3 100644
--- a/spec/frontend/branches/divergence_graph_spec.js
+++ b/spec/frontend/branches/divergence_graph_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
import init from '~/branches/divergence_graph';
+import axios from '~/lib/utils/axios_utils';
describe('Divergence graph', () => {
let mock;
diff --git a/spec/frontend/captcha/captcha_modal_spec.js b/spec/frontend/captcha/captcha_modal_spec.js
new file mode 100644
index 00000000000..b8448f9ff0a
--- /dev/null
+++ b/spec/frontend/captcha/captcha_modal_spec.js
@@ -0,0 +1,171 @@
+import { GlModal } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import { stubComponent } from 'helpers/stub_component';
+import CaptchaModal from '~/captcha/captcha_modal.vue';
+import { initRecaptchaScript } from '~/captcha/init_recaptcha_script';
+
+jest.mock('~/captcha/init_recaptcha_script');
+
+describe('Captcha Modal', () => {
+ let wrapper;
+ let modal;
+ let grecaptcha;
+
+ const captchaSiteKey = 'abc123';
+
+ function createComponent({ props = {} } = {}) {
+ wrapper = shallowMount(CaptchaModal, {
+ propsData: {
+ captchaSiteKey,
+ ...props,
+ },
+ stubs: {
+ GlModal: stubComponent(GlModal),
+ },
+ });
+ }
+
+ beforeEach(() => {
+ grecaptcha = {
+ render: jest.fn(),
+ };
+
+ initRecaptchaScript.mockResolvedValue(grecaptcha);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findGlModal = () => {
+ const glModal = wrapper.find(GlModal);
+
+ jest.spyOn(glModal.vm, 'show').mockImplementation(() => glModal.vm.$emit('shown'));
+ jest
+ .spyOn(glModal.vm, 'hide')
+ .mockImplementation(() => glModal.vm.$emit('hide', { trigger: '' }));
+
+ return glModal;
+ };
+
+ const showModal = () => {
+ wrapper.setProps({ needsCaptchaResponse: true });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ modal = findGlModal();
+ });
+
+ describe('rendering', () => {
+ it('renders', () => {
+ expect(modal.exists()).toBe(true);
+ });
+
+ it('assigns the modal a unique ID', () => {
+ const firstInstanceModalId = modal.props('modalId');
+ createComponent();
+ const secondInstanceModalId = findGlModal().props('modalId');
+ expect(firstInstanceModalId).not.toEqual(secondInstanceModalId);
+ });
+ });
+
+ describe('functionality', () => {
+ describe('when modal is shown', () => {
+ describe('when initRecaptchaScript promise resolves successfully', () => {
+ beforeEach(async () => {
+ showModal();
+
+ await nextTick();
+ });
+
+ it('shows modal', async () => {
+ expect(findGlModal().vm.show).toHaveBeenCalled();
+ });
+
+ it('renders window.grecaptcha', () => {
+ expect(grecaptcha.render).toHaveBeenCalledWith(wrapper.vm.$refs.captcha, {
+ sitekey: captchaSiteKey,
+ callback: expect.any(Function),
+ });
+ });
+
+ describe('then the user solves the captcha', () => {
+ const captchaResponse = 'a captcha response';
+
+ beforeEach(() => {
+ // simulate the grecaptcha library invoking the callback
+ const { callback } = grecaptcha.render.mock.calls[0][1];
+ callback(captchaResponse);
+ });
+
+ it('emits receivedCaptchaResponse exactly once with the captcha response', () => {
+ expect(wrapper.emitted('receivedCaptchaResponse')).toEqual([[captchaResponse]]);
+ });
+
+ it('hides modal with null trigger', async () => {
+ // Assert that hide is called with zero args, so that we don't trigger the logic
+ // for hiding the modal via cancel, esc, headerclose, etc, without a captcha response
+ expect(modal.vm.hide).toHaveBeenCalledWith();
+ });
+ });
+
+ describe('then the user hides the modal without solving the captcha', () => {
+ // Even though we don't explicitly check for these trigger values, these are the
+ // currently supported ones which can be emitted.
+ // See https://bootstrap-vue.org/docs/components/modal#prevent-closing
+ describe.each`
+ trigger | expected
+ ${'cancel'} | ${[[null]]}
+ ${'esc'} | ${[[null]]}
+ ${'backdrop'} | ${[[null]]}
+ ${'headerclose'} | ${[[null]]}
+ `('using the $trigger trigger', ({ trigger, expected }) => {
+ beforeEach(() => {
+ const bvModalEvent = {
+ trigger,
+ };
+ modal.vm.$emit('hide', bvModalEvent);
+ });
+
+ it(`emits receivedCaptchaResponse with ${JSON.stringify(expected)}`, () => {
+ expect(wrapper.emitted('receivedCaptchaResponse')).toEqual(expected);
+ });
+ });
+ });
+ });
+
+ describe('when initRecaptchaScript promise rejects', () => {
+ const fakeError = {};
+
+ beforeEach(() => {
+ initRecaptchaScript.mockImplementation(() => Promise.reject(fakeError));
+
+ jest.spyOn(console, 'error').mockImplementation();
+
+ showModal();
+ });
+
+ it('emits receivedCaptchaResponse exactly once with null', () => {
+ expect(wrapper.emitted('receivedCaptchaResponse')).toEqual([[null]]);
+ });
+
+ it('hides modal with null trigger', async () => {
+ // Assert that hide is called with zero args, so that we don't trigger the logic
+ // for hiding the modal via cancel, esc, headerclose, etc, without a captcha response
+ expect(modal.vm.hide).toHaveBeenCalledWith();
+ });
+
+ it('calls console.error with a message and the exception', () => {
+ // eslint-disable-next-line no-console
+ expect(console.error).toHaveBeenCalledWith(
+ expect.stringMatching(/exception.*captcha/),
+ fakeError,
+ );
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/captcha/init_recaptcha_script_spec.js b/spec/frontend/captcha/init_recaptcha_script_spec.js
new file mode 100644
index 00000000000..af07c9e474e
--- /dev/null
+++ b/spec/frontend/captcha/init_recaptcha_script_spec.js
@@ -0,0 +1,59 @@
+import {
+ RECAPTCHA_API_URL_PREFIX,
+ RECAPTCHA_ONLOAD_CALLBACK_NAME,
+ clearMemoizeCache,
+ initRecaptchaScript,
+} from '~/captcha/init_recaptcha_script';
+
+describe('initRecaptchaScript', () => {
+ afterEach(() => {
+ document.head.innerHTML = '';
+ clearMemoizeCache();
+ });
+
+ const getScriptOnload = () => window[RECAPTCHA_ONLOAD_CALLBACK_NAME];
+ const triggerScriptOnload = () => window[RECAPTCHA_ONLOAD_CALLBACK_NAME]();
+
+ describe('when called', () => {
+ let result;
+
+ beforeEach(() => {
+ result = initRecaptchaScript();
+ });
+
+ it('adds script to head', () => {
+ expect(document.head).toMatchInlineSnapshot(`
+ <head>
+ <script
+ class="js-recaptcha-script"
+ src="${RECAPTCHA_API_URL_PREFIX}?onload=${RECAPTCHA_ONLOAD_CALLBACK_NAME}&render=explicit"
+ />
+ </head>
+ `);
+ });
+
+ it('is memoized', () => {
+ expect(initRecaptchaScript()).toBe(result);
+ expect(document.head.querySelectorAll('script').length).toBe(1);
+ });
+
+ describe('when onload is triggered', () => {
+ beforeEach(() => {
+ window.grecaptcha = 'fake grecaptcha';
+ triggerScriptOnload();
+ });
+
+ afterEach(() => {
+ window.grecaptcha = undefined;
+ });
+
+ it('resolves promise with window.grecaptcha as argument', async () => {
+ await expect(result).resolves.toBe(window.grecaptcha);
+ });
+
+ it('sets window[RECAPTCHA_ONLOAD_CALLBACK_NAME] to undefined', async () => {
+ expect(getScriptOnload()).toBeUndefined();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci_lint/components/ci_lint_spec.js b/spec/frontend/ci_lint/components/ci_lint_spec.js
index 1c99fdb3505..8a065436da0 100644
--- a/spec/frontend/ci_lint/components/ci_lint_spec.js
+++ b/spec/frontend/ci_lint/components/ci_lint_spec.js
@@ -1,10 +1,10 @@
import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
-import EditorLite from '~/vue_shared/components/editor_lite.vue';
import CiLint from '~/ci_lint/components/ci_lint.vue';
import CiLintResults from '~/pipeline_editor/components/lint/ci_lint_results.vue';
import lintCIMutation from '~/pipeline_editor/graphql/mutations/lint_ci.mutation.graphql';
+import EditorLite from '~/vue_shared/components/editor_lite.vue';
import { mockLintDataValid } from '../mock_data';
describe('CI Lint', () => {
diff --git a/spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js b/spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js
index 30aa634fc81..41af257ad89 100644
--- a/spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js
+++ b/spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js
@@ -1,9 +1,9 @@
-import { mount } from '@vue/test-utils';
import { GlTable, GlBadge } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import TriggersList from '~/ci_settings_pipeline_triggers/components/triggers_list.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import TriggersList from '~/ci_settings_pipeline_triggers/components/triggers_list.vue';
import { triggers } from '../mock_data';
describe('TriggersList', () => {
diff --git a/spec/frontend/ci_variable_list/components/ci_environments_dropdown_spec.js b/spec/frontend/ci_variable_list/components/ci_environments_dropdown_spec.js
index faa88394447..75c6e8e4540 100644
--- a/spec/frontend/ci_variable_list/components/ci_environments_dropdown_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_environments_dropdown_spec.js
@@ -1,6 +1,6 @@
-import Vuex from 'vuex';
-import { mount, createLocalVue } from '@vue/test-utils';
import { GlDropdownItem, GlIcon } from '@gitlab/ui';
+import { mount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
import CiEnvironmentsDropdown from '~/ci_variable_list/components/ci_environments_dropdown.vue';
const localVue = createLocalVue();
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 b2e51029a06..991dc8592e9 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
@@ -1,8 +1,8 @@
-import Vuex from 'vuex';
-import { createLocalVue, shallowMount, mount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
-import { AWS_ACCESS_KEY_ID } from '~/ci_variable_list/constants';
+import { createLocalVue, shallowMount, mount } from '@vue/test-utils';
+import Vuex from 'vuex';
import CiVariableModal from '~/ci_variable_list/components/ci_variable_modal.vue';
+import { AWS_ACCESS_KEY_ID } from '~/ci_variable_list/constants';
import createStore from '~/ci_variable_list/store';
import mockData from '../services/mock_data';
import ModalStub from '../stubs';
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
index 5d37f059161..b43153d3d7c 100644
--- a/spec/frontend/ci_variable_list/components/ci_variable_popover_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_variable_popover_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
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';
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 12449fc7615..03f90f72d87 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
@@ -1,5 +1,5 @@
-import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
+import Vuex from 'vuex';
import CiVariableSettings from '~/ci_variable_list/components/ci_variable_settings.vue';
import createStore from '~/ci_variable_list/store';
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js
index fbc34528d4d..ade2d65b857 100644
--- a/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js
@@ -1,6 +1,6 @@
-import Vuex from 'vuex';
-import { createLocalVue, mount } from '@vue/test-utils';
import { GlTable } from '@gitlab/ui';
+import { createLocalVue, mount } from '@vue/test-utils';
+import Vuex from 'vuex';
import CiVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
import createStore from '~/ci_variable_list/store';
import mockData from '../services/mock_data';
diff --git a/spec/frontend/ci_variable_list/store/actions_spec.js b/spec/frontend/ci_variable_list/store/actions_spec.js
index 075e5829305..be3640936dc 100644
--- a/spec/frontend/ci_variable_list/store/actions_spec.js
+++ b/spec/frontend/ci_variable_list/store/actions_spec.js
@@ -1,13 +1,13 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
-import axios from '~/lib/utils/axios_utils';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
-import getInitialState from '~/ci_variable_list/store/state';
import * as actions from '~/ci_variable_list/store/actions';
import * as types from '~/ci_variable_list/store/mutation_types';
-import mockData from '../services/mock_data';
+import getInitialState from '~/ci_variable_list/store/state';
import { prepareDataForDisplay, prepareEnvironments } from '~/ci_variable_list/store/utils';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import mockData from '../services/mock_data';
jest.mock('~/api.js');
jest.mock('~/flash.js');
diff --git a/spec/frontend/ci_variable_list/store/mutations_spec.js b/spec/frontend/ci_variable_list/store/mutations_spec.js
index a333fb7d8f9..ae750ff426d 100644
--- a/spec/frontend/ci_variable_list/store/mutations_spec.js
+++ b/spec/frontend/ci_variable_list/store/mutations_spec.js
@@ -1,6 +1,6 @@
-import state from '~/ci_variable_list/store/state';
-import mutations from '~/ci_variable_list/store/mutations';
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;
diff --git a/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap b/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap
index ee4ec4636ea..6047b404197 100644
--- a/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap
+++ b/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap
@@ -60,8 +60,8 @@ exports[`Remove cluster confirmation modal renders splitbutton with modal includ
>
<svg
aria-hidden="true"
- class="gl-icon s16 gl-new-dropdown-item-check-icon"
- data-testid="mobile-issue-close-icon"
+ class="gl-icon s16 gl-new-dropdown-item-check-icon gl-mt-3 gl-align-self-start"
+ data-testid="dropdown-item-checkbox"
>
<use
href="#mobile-issue-close"
@@ -115,8 +115,8 @@ exports[`Remove cluster confirmation modal renders splitbutton with modal includ
>
<svg
aria-hidden="true"
- class="gl-icon s16 gl-new-dropdown-item-check-icon gl-visibility-hidden"
- data-testid="mobile-issue-close-icon"
+ class="gl-icon s16 gl-new-dropdown-item-check-icon gl-visibility-hidden gl-mt-3 gl-align-self-start"
+ data-testid="dropdown-item-checkbox"
>
<use
href="#mobile-issue-close"
diff --git a/spec/frontend/clusters/components/application_row_spec.js b/spec/frontend/clusters/components/application_row_spec.js
index 49a299cfb3c..eff3493d7bd 100644
--- a/spec/frontend/clusters/components/application_row_spec.js
+++ b/spec/frontend/clusters/components/application_row_spec.js
@@ -1,10 +1,10 @@
-import { shallowMount } from '@vue/test-utils';
import { GlSprintf } from '@gitlab/ui';
-import eventHub from '~/clusters/event_hub';
-import { APPLICATION_STATUS, ELASTIC_STACK } from '~/clusters/constants';
+import { shallowMount } from '@vue/test-utils';
import ApplicationRow from '~/clusters/components/application_row.vue';
import UninstallApplicationConfirmationModal from '~/clusters/components/uninstall_application_confirmation_modal.vue';
import UpdateApplicationConfirmationModal from '~/clusters/components/update_application_confirmation_modal.vue';
+import { APPLICATION_STATUS, ELASTIC_STACK } from '~/clusters/constants';
+import eventHub from '~/clusters/event_hub';
import { DEFAULT_APPLICATION_STATE } from '../services/mock_data';
diff --git a/spec/frontend/clusters/components/applications_spec.js b/spec/frontend/clusters/components/applications_spec.js
index cf89246c1a5..db5915cb1eb 100644
--- a/spec/frontend/clusters/components/applications_spec.js
+++ b/spec/frontend/clusters/components/applications_spec.js
@@ -1,13 +1,13 @@
import { shallowMount, mount } from '@vue/test-utils';
-import Applications from '~/clusters/components/applications.vue';
-import { CLUSTER_TYPE, PROVIDER_TYPE } from '~/clusters/constants';
-import { APPLICATIONS_MOCK_STATE } from '../services/mock_data';
-import eventHub from '~/clusters/event_hub';
import ApplicationRow from '~/clusters/components/application_row.vue';
-import KnativeDomainEditor from '~/clusters/components/knative_domain_editor.vue';
+import Applications from '~/clusters/components/applications.vue';
import CrossplaneProviderStack from '~/clusters/components/crossplane_provider_stack.vue';
-import IngressModsecuritySettings from '~/clusters/components/ingress_modsecurity_settings.vue';
import FluentdOutputSettings from '~/clusters/components/fluentd_output_settings.vue';
+import IngressModsecuritySettings from '~/clusters/components/ingress_modsecurity_settings.vue';
+import KnativeDomainEditor from '~/clusters/components/knative_domain_editor.vue';
+import { CLUSTER_TYPE, PROVIDER_TYPE } from '~/clusters/constants';
+import eventHub from '~/clusters/event_hub';
+import { APPLICATIONS_MOCK_STATE } from '../services/mock_data';
describe('Applications', () => {
let wrapper;
@@ -16,7 +16,7 @@ describe('Applications', () => {
gon.features = gon.features || {};
});
- const createApp = ({ applications, type, propsData } = {}, isShallow) => {
+ const createComponent = ({ applications, type, propsData } = {}, isShallow) => {
const mountMethod = isShallow ? shallowMount : mount;
wrapper = mountMethod(Applications, {
@@ -29,7 +29,7 @@ describe('Applications', () => {
});
};
- const createShallowApp = (options) => createApp(options, true);
+ const createShallowComponent = (options) => createComponent(options, true);
const findByTestId = (id) => wrapper.find(`[data-testid="${id}"]`);
afterEach(() => {
wrapper.destroy();
@@ -37,7 +37,7 @@ describe('Applications', () => {
describe('Project cluster applications', () => {
beforeEach(() => {
- createApp({ type: CLUSTER_TYPE.PROJECT });
+ createComponent({ type: CLUSTER_TYPE.PROJECT });
});
it('renders a row for Ingress', () => {
@@ -82,7 +82,7 @@ describe('Applications', () => {
describe('Group cluster applications', () => {
beforeEach(() => {
- createApp({ type: CLUSTER_TYPE.GROUP });
+ createComponent({ type: CLUSTER_TYPE.GROUP });
});
it('renders a row for Ingress', () => {
@@ -128,7 +128,7 @@ describe('Applications', () => {
describe('Instance cluster applications', () => {
beforeEach(() => {
- createApp({ type: CLUSTER_TYPE.INSTANCE });
+ createComponent({ type: CLUSTER_TYPE.INSTANCE });
});
it('renders a row for Ingress', () => {
@@ -174,14 +174,14 @@ describe('Applications', () => {
describe('Helm application', () => {
it('does not render a row for Helm Tiller', () => {
- createApp();
+ createComponent();
expect(wrapper.find('.js-cluster-application-row-helm').exists()).toBe(false);
});
});
describe('Ingress application', () => {
it('shows the correct warning message', () => {
- createApp();
+ createComponent();
expect(findByTestId('ingressCostWarning').element).toMatchSnapshot();
});
@@ -195,7 +195,7 @@ describe('Applications', () => {
},
};
- beforeEach(() => createShallowApp(propsData));
+ beforeEach(() => createShallowComponent(propsData));
it('renders IngressModsecuritySettings', () => {
const modsecuritySettings = wrapper.find(IngressModsecuritySettings);
@@ -206,7 +206,7 @@ describe('Applications', () => {
describe('when installed', () => {
describe('with ip address', () => {
it('renders ip address with a clipboard button', () => {
- createApp({
+ createComponent({
applications: {
ingress: {
title: 'Ingress',
@@ -225,7 +225,7 @@ describe('Applications', () => {
describe('with hostname', () => {
it('renders hostname with a clipboard button', () => {
- createApp({
+ createComponent({
applications: {
ingress: {
title: 'Ingress',
@@ -255,7 +255,7 @@ describe('Applications', () => {
describe('without ip address', () => {
it('renders an input text with a loading icon and an alert text', () => {
- createApp({
+ createComponent({
applications: {
ingress: {
title: 'Ingress',
@@ -272,7 +272,7 @@ describe('Applications', () => {
describe('before installing', () => {
it('does not render the IP address', () => {
- createApp();
+ createComponent();
expect(wrapper.text()).not.toContain('Ingress IP Address');
expect(wrapper.find('.js-endpoint').exists()).toBe(false);
@@ -282,13 +282,13 @@ describe('Applications', () => {
describe('Cert-Manager application', () => {
it('shows the correct description', () => {
- createApp();
+ createComponent();
expect(findByTestId('certManagerDescription').element).toMatchSnapshot();
});
describe('when not installed', () => {
it('renders email & allows editing', () => {
- createApp({
+ createComponent({
applications: {
cert_manager: {
title: 'Cert-Manager',
@@ -305,7 +305,7 @@ describe('Applications', () => {
describe('when installed', () => {
it('renders email in readonly', () => {
- createApp({
+ createComponent({
applications: {
cert_manager: {
title: 'Cert-Manager',
@@ -324,7 +324,7 @@ describe('Applications', () => {
describe('Jupyter application', () => {
describe('with ingress installed with ip & jupyter installable', () => {
it('renders hostname active input', () => {
- createApp({
+ createComponent({
applications: {
ingress: {
title: 'Ingress',
@@ -342,7 +342,7 @@ describe('Applications', () => {
describe('with ingress installed without external ip', () => {
it('does not render hostname input', () => {
- createApp({
+ createComponent({
applications: {
ingress: { title: 'Ingress', status: 'installed' },
},
@@ -356,7 +356,7 @@ describe('Applications', () => {
describe('with ingress & jupyter installed', () => {
it('renders readonly input', () => {
- createApp({
+ createComponent({
applications: {
ingress: {
title: 'Ingress',
@@ -375,7 +375,7 @@ describe('Applications', () => {
describe('without ingress installed', () => {
beforeEach(() => {
- createApp();
+ createComponent();
});
it('does not render input', () => {
@@ -388,7 +388,7 @@ describe('Applications', () => {
describe('Prometheus application', () => {
it('shows the correct description', () => {
- createApp();
+ createComponent();
expect(findByTestId('prometheusDescription').element).toMatchSnapshot();
});
});
@@ -414,14 +414,14 @@ describe('Applications', () => {
let knativeDomainEditor;
beforeEach(() => {
- createShallowApp(propsData);
+ createShallowComponent(propsData);
jest.spyOn(eventHub, '$emit');
knativeDomainEditor = wrapper.find(KnativeDomainEditor);
});
it('shows the correct description', async () => {
- createApp();
+ createComponent();
wrapper.setProps({
providerType: PROVIDER_TYPE.GCP,
preInstalledKnative: true,
@@ -487,7 +487,7 @@ describe('Applications', () => {
},
};
- beforeEach(() => createShallowApp(propsData));
+ beforeEach(() => createShallowComponent(propsData));
it('renders the correct Component', () => {
const crossplane = wrapper.find(CrossplaneProviderStack);
@@ -495,7 +495,7 @@ describe('Applications', () => {
});
it('shows the correct description', () => {
- createApp();
+ createComponent();
expect(findByTestId('crossplaneDescription').element).toMatchSnapshot();
});
});
@@ -503,7 +503,7 @@ describe('Applications', () => {
describe('Elastic Stack application', () => {
describe('with elastic stack installable', () => {
it('renders the install button enabled', () => {
- createApp();
+ createComponent();
expect(
wrapper
@@ -517,7 +517,7 @@ describe('Applications', () => {
describe('elastic stack installed', () => {
it('renders uninstall button', () => {
- createApp({
+ createComponent({
applications: {
elastic_stack: { title: 'Elastic Stack', status: 'installed' },
},
@@ -535,7 +535,7 @@ describe('Applications', () => {
});
describe('Fluentd application', () => {
- beforeEach(() => createShallowApp());
+ beforeEach(() => createShallowComponent());
it('renders the correct Component', () => {
expect(wrapper.find(FluentdOutputSettings).exists()).toBe(true);
@@ -544,7 +544,7 @@ describe('Applications', () => {
describe('Cilium application', () => {
it('shows the correct description', () => {
- createApp({ propsData: { ciliumHelpPath: 'cilium-help-path' } });
+ createComponent({ propsData: { ciliumHelpPath: 'cilium-help-path' } });
expect(findByTestId('ciliumDescription').element).toMatchSnapshot();
});
});
diff --git a/spec/frontend/clusters/components/fluentd_output_settings_spec.js b/spec/frontend/clusters/components/fluentd_output_settings_spec.js
index cd996ae915b..2c6e5bbd46a 100644
--- a/spec/frontend/clusters/components/fluentd_output_settings_spec.js
+++ b/spec/frontend/clusters/components/fluentd_output_settings_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlAlert, GlDropdown, GlFormCheckbox } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import FluentdOutputSettings from '~/clusters/components/fluentd_output_settings.vue';
import { APPLICATION_STATUS, FLUENTD } from '~/clusters/constants';
import eventHub from '~/clusters/event_hub';
diff --git a/spec/frontend/clusters/components/ingress_modsecurity_settings_spec.js b/spec/frontend/clusters/components/ingress_modsecurity_settings_spec.js
index 1f07a0b7908..fd6d9854868 100644
--- a/spec/frontend/clusters/components/ingress_modsecurity_settings_spec.js
+++ b/spec/frontend/clusters/components/ingress_modsecurity_settings_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlAlert, GlToggle, GlDropdown } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import IngressModsecuritySettings from '~/clusters/components/ingress_modsecurity_settings.vue';
import { APPLICATION_STATUS, INGRESS } from '~/clusters/constants';
import eventHub from '~/clusters/event_hub';
diff --git a/spec/frontend/clusters/components/knative_domain_editor_spec.js b/spec/frontend/clusters/components/knative_domain_editor_spec.js
index b7f76211fd6..207eb071171 100644
--- a/spec/frontend/clusters/components/knative_domain_editor_spec.js
+++ b/spec/frontend/clusters/components/knative_domain_editor_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlDropdownItem, GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import KnativeDomainEditor from '~/clusters/components/knative_domain_editor.vue';
import { APPLICATION_STATUS } from '~/clusters/constants';
diff --git a/spec/frontend/clusters/components/new_cluster_spec.js b/spec/frontend/clusters/components/new_cluster_spec.js
index bb4898f98ba..e4bca5eaaa5 100644
--- a/spec/frontend/clusters/components/new_cluster_spec.js
+++ b/spec/frontend/clusters/components/new_cluster_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlLink, GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import NewCluster from '~/clusters/components/new_cluster.vue';
import createClusterStore from '~/clusters/stores/new_cluster';
diff --git a/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js b/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js
index f448948843a..e2726b93ea5 100644
--- a/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js
+++ b/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js
@@ -1,7 +1,7 @@
-import { mount } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui';
-import SplitButton from '~/vue_shared/components/split_button.vue';
+import { mount } from '@vue/test-utils';
import RemoveClusterConfirmation from '~/clusters/components/remove_cluster_confirmation.vue';
+import SplitButton from '~/vue_shared/components/split_button.vue';
describe('Remove cluster confirmation modal', () => {
let wrapper;
diff --git a/spec/frontend/clusters/components/uninstall_application_button_spec.js b/spec/frontend/clusters/components/uninstall_application_button_spec.js
index c106292965e..2596820e5ac 100644
--- a/spec/frontend/clusters/components/uninstall_application_button_spec.js
+++ b/spec/frontend/clusters/components/uninstall_application_button_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import UninstallApplicationButton from '~/clusters/components/uninstall_application_button.vue';
import { APPLICATION_STATUS } from '~/clusters/constants';
diff --git a/spec/frontend/clusters/components/uninstall_application_confirmation_modal_spec.js b/spec/frontend/clusters/components/uninstall_application_confirmation_modal_spec.js
index c07f6851826..74ae4ecc486 100644
--- a/spec/frontend/clusters/components/uninstall_application_confirmation_modal_spec.js
+++ b/spec/frontend/clusters/components/uninstall_application_confirmation_modal_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import UninstallApplicationConfirmationModal from '~/clusters/components/uninstall_application_confirmation_modal.vue';
import { INGRESS } from '~/clusters/constants';
diff --git a/spec/frontend/clusters/components/update_application_confirmation_modal_spec.js b/spec/frontend/clusters/components/update_application_confirmation_modal_spec.js
index dd3aaf6f946..e933f17a980 100644
--- a/spec/frontend/clusters/components/update_application_confirmation_modal_spec.js
+++ b/spec/frontend/clusters/components/update_application_confirmation_modal_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import UpdateApplicationConfirmationModal from '~/clusters/components/update_application_confirmation_modal.vue';
import { ELASTIC_STACK } from '~/clusters/constants';
diff --git a/spec/frontend/clusters/forms/components/integration_form_spec.js b/spec/frontend/clusters/forms/components/integration_form_spec.js
index 3a3700eb0b7..0323245244d 100644
--- a/spec/frontend/clusters/forms/components/integration_form_spec.js
+++ b/spec/frontend/clusters/forms/components/integration_form_spec.js
@@ -1,6 +1,6 @@
-import Vuex from 'vuex';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlToggle, GlButton } from '@gitlab/ui';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
import IntegrationForm from '~/clusters/forms/components/integration_form.vue';
import { createStore } from '~/clusters/forms/stores/index';
diff --git a/spec/frontend/clusters/services/application_state_machine_spec.js b/spec/frontend/clusters/services/application_state_machine_spec.js
index a3850a64f3b..55230625ba4 100644
--- a/spec/frontend/clusters/services/application_state_machine_spec.js
+++ b/spec/frontend/clusters/services/application_state_machine_spec.js
@@ -1,10 +1,10 @@
-import transitionApplicationState from '~/clusters/services/application_state_machine';
import {
APPLICATION_STATUS,
UNINSTALL_EVENT,
UPDATE_EVENT,
INSTALL_EVENT,
} from '~/clusters/constants';
+import transitionApplicationState from '~/clusters/services/application_state_machine';
const {
NO_STATUS,
diff --git a/spec/frontend/clusters/services/crossplane_provider_stack_spec.js b/spec/frontend/clusters/services/crossplane_provider_stack_spec.js
index 3e5f8de8e7b..f95b175ca64 100644
--- a/spec/frontend/clusters/services/crossplane_provider_stack_spec.js
+++ b/spec/frontend/clusters/services/crossplane_provider_stack_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlDropdownItem, GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import CrossplaneProviderStack from '~/clusters/components/crossplane_provider_stack.vue';
describe('CrossplaneProviderStack component', () => {
diff --git a/spec/frontend/clusters/stores/clusters_store_spec.js b/spec/frontend/clusters/stores/clusters_store_spec.js
index df10d9af273..c80949531c8 100644
--- a/spec/frontend/clusters/stores/clusters_store_spec.js
+++ b/spec/frontend/clusters/stores/clusters_store_spec.js
@@ -1,5 +1,5 @@
-import ClustersStore from '~/clusters/stores/clusters_store';
import { APPLICATION_INSTALLED_STATUSES, APPLICATION_STATUS, RUNNER } from '~/clusters/constants';
+import ClustersStore from '~/clusters/stores/clusters_store';
import { CLUSTERS_MOCK_DATA } from '../services/mock_data';
describe('Clusters Store', () => {
diff --git a/spec/frontend/clusters_list/components/ancestor_notice_spec.js b/spec/frontend/clusters_list/components/ancestor_notice_spec.js
index 79f917d4601..c7ee2a00f5b 100644
--- a/spec/frontend/clusters_list/components/ancestor_notice_spec.js
+++ b/spec/frontend/clusters_list/components/ancestor_notice_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlLink, GlSprintf, GlAlert } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import AncestorNotice from '~/clusters_list/components/ancestor_notice.vue';
import ClusterStore from '~/clusters_list/store';
diff --git a/spec/frontend/clusters_list/components/clusters_spec.js b/spec/frontend/clusters_list/components/clusters_spec.js
index d61f79071d5..f398d7a0965 100644
--- a/spec/frontend/clusters_list/components/clusters_spec.js
+++ b/spec/frontend/clusters_list/components/clusters_spec.js
@@ -1,15 +1,15 @@
-import MockAdapter from 'axios-mock-adapter';
-import { mount } from '@vue/test-utils';
import {
GlLoadingIcon,
GlPagination,
GlDeprecatedSkeletonLoading as GlSkeletonLoading,
GlTable,
} from '@gitlab/ui';
-import * as Sentry from '~/sentry/wrapper';
-import axios from '~/lib/utils/axios_utils';
+import { mount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
import Clusters from '~/clusters_list/components/clusters.vue';
import ClusterStore from '~/clusters_list/store';
+import axios from '~/lib/utils/axios_utils';
+import * as Sentry from '~/sentry/wrapper';
import { apiData } from '../mock_data';
describe('Clusters', () => {
diff --git a/spec/frontend/clusters_list/components/node_error_help_text_spec.js b/spec/frontend/clusters_list/components/node_error_help_text_spec.js
index fa566c2dc57..18d27f3fd80 100644
--- a/spec/frontend/clusters_list/components/node_error_help_text_spec.js
+++ b/spec/frontend/clusters_list/components/node_error_help_text_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlPopover } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import NodeErrorHelpText from '~/clusters_list/components/node_error_help_text.vue';
describe('NodeErrorHelpText', () => {
diff --git a/spec/frontend/clusters_list/store/actions_spec.js b/spec/frontend/clusters_list/store/actions_spec.js
index 6214cb50e13..00b998166aa 100644
--- a/spec/frontend/clusters_list/store/actions_spec.js
+++ b/spec/frontend/clusters_list/store/actions_spec.js
@@ -1,14 +1,14 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import * as Sentry from '~/sentry/wrapper';
-import Poll from '~/lib/utils/poll';
+import { MAX_REQUESTS } from '~/clusters_list/constants';
+import * as actions from '~/clusters_list/store/actions';
+import * as types from '~/clusters_list/store/mutation_types';
import { deprecatedCreateFlash as flashError } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import Poll from '~/lib/utils/poll';
+import * as Sentry from '~/sentry/wrapper';
import { apiData } from '../mock_data';
-import { MAX_REQUESTS } from '~/clusters_list/constants';
-import * as types from '~/clusters_list/store/mutation_types';
-import * as actions from '~/clusters_list/store/actions';
jest.mock('~/flash.js');
diff --git a/spec/frontend/clusters_list/store/mutations_spec.js b/spec/frontend/clusters_list/store/mutations_spec.js
index df0dfe587b6..c0fe634a703 100644
--- a/spec/frontend/clusters_list/store/mutations_spec.js
+++ b/spec/frontend/clusters_list/store/mutations_spec.js
@@ -1,7 +1,7 @@
import * as types from '~/clusters_list/store/mutation_types';
-import { apiData } from '../mock_data';
-import getInitialState from '~/clusters_list/store/state';
import mutations from '~/clusters_list/store/mutations';
+import getInitialState from '~/clusters_list/store/state';
+import { apiData } from '../mock_data';
describe('Admin statistics panel mutations', () => {
let state;
diff --git a/spec/frontend/code_navigation/components/app_spec.js b/spec/frontend/code_navigation/components/app_spec.js
index 6dfc81dcc40..ea389fa35c0 100644
--- a/spec/frontend/code_navigation/components/app_spec.js
+++ b/spec/frontend/code_navigation/components/app_spec.js
@@ -1,8 +1,8 @@
-import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
-import createState from '~/code_navigation/store/state';
+import Vuex from 'vuex';
import App from '~/code_navigation/components/app.vue';
import Popover from '~/code_navigation/components/popover.vue';
+import createState from '~/code_navigation/store/state';
const localVue = createLocalVue();
const fetchData = jest.fn();
diff --git a/spec/frontend/code_navigation/components/popover_spec.js b/spec/frontend/code_navigation/components/popover_spec.js
index 7b323cfab72..c038c04a0f8 100644
--- a/spec/frontend/code_navigation/components/popover_spec.js
+++ b/spec/frontend/code_navigation/components/popover_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import Popover from '~/code_navigation/components/popover.vue';
import DocLine from '~/code_navigation/components/doc_line.vue';
+import Popover from '~/code_navigation/components/popover.vue';
const DEFINITION_PATH_PREFIX = 'http://gitlab.com';
diff --git a/spec/frontend/code_navigation/store/actions_spec.js b/spec/frontend/code_navigation/store/actions_spec.js
index 39cf4acd5ce..73f935deeca 100644
--- a/spec/frontend/code_navigation/store/actions_spec.js
+++ b/spec/frontend/code_navigation/store/actions_spec.js
@@ -1,8 +1,8 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import actions from '~/code_navigation/store/actions';
-import axios from '~/lib/utils/axios_utils';
import { setCurrentHoverElement, addInteractionClass } from '~/code_navigation/utils';
+import axios from '~/lib/utils/axios_utils';
jest.mock('~/code_navigation/utils');
diff --git a/spec/frontend/collapsed_sidebar_todo_spec.js b/spec/frontend/collapsed_sidebar_todo_spec.js
index cc89a3c68f0..ef53cc9e103 100644
--- a/spec/frontend/collapsed_sidebar_todo_spec.js
+++ b/spec/frontend/collapsed_sidebar_todo_spec.js
@@ -1,10 +1,13 @@
/* eslint-disable no-new */
-import { clone } from 'lodash';
import MockAdapter from 'axios-mock-adapter';
-import { TEST_HOST } from 'spec/test_constants';
+import { clone } from 'lodash';
import waitForPromises from 'helpers/wait_for_promises';
+import { TEST_HOST } from 'spec/test_constants';
import axios from '~/lib/utils/axios_utils';
import Sidebar from '~/right_sidebar';
+import { fixTitle } from '~/tooltips';
+
+jest.mock('~/tooltips');
describe('Issuable right sidebar collapsed todo toggle', () => {
const fixtureName = 'issues/open-issue.html';
@@ -96,11 +99,10 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
setImmediate(() => {
- expect(
- document
- .querySelector('.js-issuable-todo.sidebar-collapsed-icon')
- .getAttribute('data-original-title'),
- ).toBe('Mark as done');
+ const el = document.querySelector('.js-issuable-todo.sidebar-collapsed-icon');
+
+ expect(el.getAttribute('title')).toBe('Mark as done');
+ expect(fixTitle).toHaveBeenCalledWith(el);
done();
});
diff --git a/spec/frontend/commit/commit_pipeline_status_component_spec.js b/spec/frontend/commit/commit_pipeline_status_component_spec.js
index 6e4368b5de8..a56f761269a 100644
--- a/spec/frontend/commit/commit_pipeline_status_component_spec.js
+++ b/spec/frontend/commit/commit_pipeline_status_component_spec.js
@@ -1,11 +1,11 @@
-import Visibility from 'visibilityjs';
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import Visibility from 'visibilityjs';
import { getJSONFixture } from 'helpers/fixtures';
-import Poll from '~/lib/utils/poll';
import { deprecatedCreateFlash as flash } from '~/flash';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import Poll from '~/lib/utils/poll';
import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
jest.mock('~/lib/utils/poll');
jest.mock('visibilityjs');
@@ -141,8 +141,8 @@ describe('Commit pipeline status component', () => {
expect(findLink().attributes('href')).toEqual(mockCiStatus.details_path);
});
- it('renders CI icon', () => {
- expect(findCiIcon().attributes('title')).toEqual('Pipeline: pending');
+ it('renders CI icon with the correct title and status', () => {
+ expect(findCiIcon().attributes('title')).toEqual('Pipeline: passed');
expect(findCiIcon().props('status')).toEqual(mockCiStatus);
});
});
diff --git a/spec/frontend/commit/pipelines/pipelines_spec.js b/spec/frontend/commit/pipelines/pipelines_spec.js
index 15b1f224699..f8bdd00f5da 100644
--- a/spec/frontend/commit/pipelines/pipelines_spec.js
+++ b/spec/frontend/commit/pipelines/pipelines_spec.js
@@ -1,9 +1,9 @@
-import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
+import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
-import axios from '~/lib/utils/axios_utils';
import Api from '~/api';
import pipelinesTable from '~/commit/pipelines/pipelines_table.vue';
+import axios from '~/lib/utils/axios_utils';
describe('Pipelines table in Commits and Merge requests', () => {
const jsonFixtureName = 'pipelines/pipelines.json';
diff --git a/spec/frontend/commits_spec.js b/spec/frontend/commits_spec.js
index e1b947ddd0d..954025091cf 100644
--- a/spec/frontend/commits_spec.js
+++ b/spec/frontend/commits_spec.js
@@ -1,8 +1,8 @@
+import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import 'vendor/jquery.endless-scroll';
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
import CommitsList from '~/commits';
+import axios from '~/lib/utils/axios_utils';
import Pager from '~/pager';
describe('Commits List', () => {
diff --git a/spec/frontend/commons/nav/user_merge_requests_spec.js b/spec/frontend/commons/nav/user_merge_requests_spec.js
index c441668f7c7..8f974051232 100644
--- a/spec/frontend/commons/nav/user_merge_requests_spec.js
+++ b/spec/frontend/commons/nav/user_merge_requests_spec.js
@@ -1,9 +1,9 @@
+import * as UserApi from '~/api/user_api';
import {
openUserCountsBroadcast,
closeUserCountsBroadcast,
refreshUserMergeRequestCounts,
} from '~/commons/nav/user_merge_requests';
-import * as UserApi from '~/api/user_api';
jest.mock('~/api');
diff --git a/spec/frontend/confidential_merge_request/components/dropdown_spec.js b/spec/frontend/confidential_merge_request/components/dropdown_spec.js
index 401948e24e4..14a0b98a0d5 100644
--- a/spec/frontend/confidential_merge_request/components/dropdown_spec.js
+++ b/spec/frontend/confidential_merge_request/components/dropdown_spec.js
@@ -1,5 +1,5 @@
-import { mount } from '@vue/test-utils';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import Dropdown from '~/confidential_merge_request/components/dropdown.vue';
let vm;
diff --git a/spec/frontend/confidential_merge_request/components/project_form_group_spec.js b/spec/frontend/confidential_merge_request/components/project_form_group_spec.js
index 975701ebd96..67f6d360f52 100644
--- a/spec/frontend/confidential_merge_request/components/project_form_group_spec.js
+++ b/spec/frontend/confidential_merge_request/components/project_form_group_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
import ProjectFormGroup from '~/confidential_merge_request/components/project_form_group.vue';
+import axios from '~/lib/utils/axios_utils';
const mockData = [
{
diff --git a/spec/frontend/contributors/component/contributors_spec.js b/spec/frontend/contributors/component/contributors_spec.js
index 24816e4e8ac..de55be4aa72 100644
--- a/spec/frontend/contributors/component/contributors_spec.js
+++ b/spec/frontend/contributors/component/contributors_spec.js
@@ -1,9 +1,9 @@
-import Vue from 'vue';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import Vue from 'vue';
+import ContributorsCharts from '~/contributors/components/contributors.vue';
import { createStore } from '~/contributors/stores';
import axios from '~/lib/utils/axios_utils';
-import ContributorsCharts from '~/contributors/components/contributors.vue';
let wrapper;
let mock;
diff --git a/spec/frontend/contributors/store/actions_spec.js b/spec/frontend/contributors/store/actions_spec.js
index 7d2f93c4940..82b6492b779 100644
--- a/spec/frontend/contributors/store/actions_spec.js
+++ b/spec/frontend/contributors/store/actions_spec.js
@@ -1,9 +1,9 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import axios from '~/lib/utils/axios_utils';
-import { deprecatedCreateFlash as flashError } from '~/flash';
import * as actions from '~/contributors/stores/actions';
import * as types from '~/contributors/stores/mutation_types';
+import { deprecatedCreateFlash as flashError } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
jest.mock('~/flash.js');
diff --git a/spec/frontend/contributors/store/mutations_spec.js b/spec/frontend/contributors/store/mutations_spec.js
index e9e756d4a65..e8da1a48c5c 100644
--- a/spec/frontend/contributors/store/mutations_spec.js
+++ b/spec/frontend/contributors/store/mutations_spec.js
@@ -1,6 +1,6 @@
-import state from '~/contributors/stores/state';
-import mutations from '~/contributors/stores/mutations';
import * as types from '~/contributors/stores/mutation_types';
+import mutations from '~/contributors/stores/mutations';
+import state from '~/contributors/stores/state';
describe('Contributors mutations', () => {
let stateCopy;
diff --git a/spec/frontend/create_cluster/components/cluster_form_dropdown_spec.js b/spec/frontend/create_cluster/components/cluster_form_dropdown_spec.js
index 90c0e2d7827..0c6095e601f 100644
--- a/spec/frontend/create_cluster/components/cluster_form_dropdown_spec.js
+++ b/spec/frontend/create_cluster/components/cluster_form_dropdown_spec.js
@@ -1,7 +1,7 @@
+import { GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import $ from 'jquery';
-import { GlIcon } from '@gitlab/ui';
import ClusterFormDropdown from '~/create_cluster/components/cluster_form_dropdown.vue';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
diff --git a/spec/frontend/create_cluster/eks_cluster/components/create_eks_cluster_spec.js b/spec/frontend/create_cluster/eks_cluster/components/create_eks_cluster_spec.js
index e0913fe2e88..95810e882a1 100644
--- a/spec/frontend/create_cluster/eks_cluster/components/create_eks_cluster_spec.js
+++ b/spec/frontend/create_cluster/eks_cluster/components/create_eks_cluster_spec.js
@@ -1,5 +1,5 @@
-import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
import CreateEksCluster from '~/create_cluster/eks_cluster/components/create_eks_cluster.vue';
import EksClusterConfigurationForm from '~/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue';
diff --git a/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js b/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js
index a4835f8c1c1..53a6f12c381 100644
--- a/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js
+++ b/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js
@@ -1,7 +1,7 @@
+import { GlFormCheckbox } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
-import Vuex from 'vuex';
import Vue from 'vue';
-import { GlFormCheckbox } from '@gitlab/ui';
+import Vuex from 'vuex';
import EksClusterConfigurationForm from '~/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue';
import eksClusterFormState from '~/create_cluster/eks_cluster/store/state';
diff --git a/spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js b/spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js
index d2d6db31d1b..d866ffd4efb 100644
--- a/spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js
+++ b/spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js
@@ -1,6 +1,6 @@
-import Vuex from 'vuex';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
import ServiceCredentialsForm from '~/create_cluster/eks_cluster/components/service_credentials_form.vue';
import eksClusterState from '~/create_cluster/eks_cluster/store/state';
diff --git a/spec/frontend/create_cluster/eks_cluster/services/aws_services_facade_spec.js b/spec/frontend/create_cluster/eks_cluster/services/aws_services_facade_spec.js
index 2853d18e2cb..7b93b6d0a09 100644
--- a/spec/frontend/create_cluster/eks_cluster/services/aws_services_facade_spec.js
+++ b/spec/frontend/create_cluster/eks_cluster/services/aws_services_facade_spec.js
@@ -1,5 +1,5 @@
-import AWS from 'aws-sdk/global';
import EC2 from 'aws-sdk/clients/ec2';
+import AWS from 'aws-sdk/global';
import {
setAWSConfig,
fetchRoles,
diff --git a/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js b/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js
index 35348d3a03b..f10cf4b4140 100644
--- a/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js
+++ b/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js
@@ -1,7 +1,7 @@
-import testAction from 'helpers/vuex_action_helper';
-import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import MockAdapter from 'axios-mock-adapter';
-import createState from '~/create_cluster/eks_cluster/store/state';
+import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
+import testAction from 'helpers/vuex_action_helper';
+import { DEFAULT_REGION } from '~/create_cluster/eks_cluster/constants';
import * as actions from '~/create_cluster/eks_cluster/store/actions';
import {
SET_CLUSTER_NAME,
@@ -23,9 +23,9 @@ import {
REQUEST_CREATE_CLUSTER,
CREATE_CLUSTER_ERROR,
} from '~/create_cluster/eks_cluster/store/mutation_types';
-import { DEFAULT_REGION } from '~/create_cluster/eks_cluster/constants';
-import axios from '~/lib/utils/axios_utils';
+import createState from '~/create_cluster/eks_cluster/store/state';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
jest.mock('~/flash');
diff --git a/spec/frontend/create_cluster/eks_cluster/store/mutations_spec.js b/spec/frontend/create_cluster/eks_cluster/store/mutations_spec.js
index 633cea595d1..54d66e79be7 100644
--- a/spec/frontend/create_cluster/eks_cluster/store/mutations_spec.js
+++ b/spec/frontend/create_cluster/eks_cluster/store/mutations_spec.js
@@ -17,8 +17,8 @@ import {
REQUEST_CREATE_CLUSTER,
CREATE_CLUSTER_ERROR,
} from '~/create_cluster/eks_cluster/store/mutation_types';
-import createState from '~/create_cluster/eks_cluster/store/state';
import mutations from '~/create_cluster/eks_cluster/store/mutations';
+import createState from '~/create_cluster/eks_cluster/store/state';
describe('Create EKS cluster store mutations', () => {
let clusterName;
diff --git a/spec/frontend/create_cluster/gke_cluster/components/gke_machine_type_dropdown_spec.js b/spec/frontend/create_cluster/gke_cluster/components/gke_machine_type_dropdown_spec.js
index c09eaa63d4d..8f4903dd91b 100644
--- a/spec/frontend/create_cluster/gke_cluster/components/gke_machine_type_dropdown_spec.js
+++ b/spec/frontend/create_cluster/gke_cluster/components/gke_machine_type_dropdown_spec.js
@@ -1,10 +1,10 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import { selectedMachineTypeMock, gapiMachineTypesResponseMock } from '../mock_data';
+import GkeMachineTypeDropdown from '~/create_cluster/gke_cluster/components/gke_machine_type_dropdown.vue';
import createState from '~/create_cluster/gke_cluster/store/state';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
-import GkeMachineTypeDropdown from '~/create_cluster/gke_cluster/components/gke_machine_type_dropdown.vue';
+import { selectedMachineTypeMock, gapiMachineTypesResponseMock } from '../mock_data';
const componentConfig = {
fieldId: 'cluster_provider_gcp_attributes_gcp_machine_type',
diff --git a/spec/frontend/create_cluster/gke_cluster/components/gke_network_dropdown_spec.js b/spec/frontend/create_cluster/gke_cluster/components/gke_network_dropdown_spec.js
index ce24d186511..23a56766037 100644
--- a/spec/frontend/create_cluster/gke_cluster/components/gke_network_dropdown_spec.js
+++ b/spec/frontend/create_cluster/gke_cluster/components/gke_network_dropdown_spec.js
@@ -1,7 +1,7 @@
-import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
-import GkeNetworkDropdown from '~/create_cluster/gke_cluster/components/gke_network_dropdown.vue';
+import Vuex from 'vuex';
import ClusterFormDropdown from '~/create_cluster/components/cluster_form_dropdown.vue';
+import GkeNetworkDropdown from '~/create_cluster/gke_cluster/components/gke_network_dropdown.vue';
import createClusterDropdownState from '~/create_cluster/store/cluster_dropdown/state';
const localVue = createLocalVue();
diff --git a/spec/frontend/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js b/spec/frontend/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js
index eb58108bf3c..b191b107609 100644
--- a/spec/frontend/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js
+++ b/spec/frontend/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js
@@ -1,10 +1,10 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import createState from '~/create_cluster/gke_cluster/store/state';
-import { selectedProjectMock, gapiProjectsResponseMock } from '../mock_data';
import GkeProjectIdDropdown from '~/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue';
+import createState from '~/create_cluster/gke_cluster/store/state';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
+import { selectedProjectMock, gapiProjectsResponseMock } from '../mock_data';
const componentConfig = {
docsUrl: 'https://console.cloud.google.com/home/dashboard',
diff --git a/spec/frontend/create_cluster/gke_cluster/components/gke_submit_button_spec.js b/spec/frontend/create_cluster/gke_cluster/components/gke_submit_button_spec.js
index 9401ba83ef4..014ed6013bd 100644
--- a/spec/frontend/create_cluster/gke_cluster/components/gke_submit_button_spec.js
+++ b/spec/frontend/create_cluster/gke_cluster/components/gke_submit_button_spec.js
@@ -1,5 +1,5 @@
-import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
import GkeSubmitButton from '~/create_cluster/gke_cluster/components/gke_submit_button.vue';
const localVue = createLocalVue();
diff --git a/spec/frontend/create_cluster/gke_cluster/components/gke_subnetwork_dropdown_spec.js b/spec/frontend/create_cluster/gke_cluster/components/gke_subnetwork_dropdown_spec.js
index 35e43d5b033..cfa8a678a9b 100644
--- a/spec/frontend/create_cluster/gke_cluster/components/gke_subnetwork_dropdown_spec.js
+++ b/spec/frontend/create_cluster/gke_cluster/components/gke_subnetwork_dropdown_spec.js
@@ -1,7 +1,7 @@
-import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
-import GkeSubnetworkDropdown from '~/create_cluster/gke_cluster/components/gke_subnetwork_dropdown.vue';
+import Vuex from 'vuex';
import ClusterFormDropdown from '~/create_cluster/components/cluster_form_dropdown.vue';
+import GkeSubnetworkDropdown from '~/create_cluster/gke_cluster/components/gke_subnetwork_dropdown.vue';
import createClusterDropdownState from '~/create_cluster/store/cluster_dropdown/state';
const localVue = createLocalVue();
diff --git a/spec/frontend/create_cluster/gke_cluster/components/gke_zone_dropdown_spec.js b/spec/frontend/create_cluster/gke_cluster/components/gke_zone_dropdown_spec.js
index c07e3f81964..4054b768e34 100644
--- a/spec/frontend/create_cluster/gke_cluster/components/gke_zone_dropdown_spec.js
+++ b/spec/frontend/create_cluster/gke_cluster/components/gke_zone_dropdown_spec.js
@@ -1,13 +1,13 @@
import { shallowMount } from '@vue/test-utils';
import GkeZoneDropdown from '~/create_cluster/gke_cluster/components/gke_zone_dropdown.vue';
-import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
-import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
import { createStore } from '~/create_cluster/gke_cluster/store';
import {
SET_PROJECT,
SET_ZONES,
SET_PROJECT_BILLING_STATUS,
} from '~/create_cluster/gke_cluster/store/mutation_types';
+import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
+import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
import { selectedZoneMock, selectedProjectMock, gapiZonesResponseMock } from '../mock_data';
const propsData = {
diff --git a/spec/frontend/create_cluster/gke_cluster/stores/actions_spec.js b/spec/frontend/create_cluster/gke_cluster/stores/actions_spec.js
index 8852baafec7..55c502b96bb 100644
--- a/spec/frontend/create_cluster/gke_cluster/stores/actions_spec.js
+++ b/spec/frontend/create_cluster/gke_cluster/stores/actions_spec.js
@@ -1,7 +1,7 @@
import testAction from 'helpers/vuex_action_helper';
-import createState from '~/create_cluster/gke_cluster/store/state';
-import * as types from '~/create_cluster/gke_cluster/store/mutation_types';
import * as actions from '~/create_cluster/gke_cluster/store/actions';
+import * as types from '~/create_cluster/gke_cluster/store/mutation_types';
+import createState from '~/create_cluster/gke_cluster/store/state';
import gapi from '../helpers';
import {
selectedProjectMock,
diff --git a/spec/frontend/create_cluster/gke_cluster/stores/mutations_spec.js b/spec/frontend/create_cluster/gke_cluster/stores/mutations_spec.js
index 2a742b6ed8f..4493d49af43 100644
--- a/spec/frontend/create_cluster/gke_cluster/stores/mutations_spec.js
+++ b/spec/frontend/create_cluster/gke_cluster/stores/mutations_spec.js
@@ -1,6 +1,6 @@
-import createState from '~/create_cluster/gke_cluster/store/state';
import * as types from '~/create_cluster/gke_cluster/store/mutation_types';
import mutations from '~/create_cluster/gke_cluster/store/mutations';
+import createState from '~/create_cluster/gke_cluster/store/state';
import {
gapiProjectsResponseMock,
gapiZonesResponseMock,
diff --git a/spec/frontend/create_cluster/init_create_cluster_spec.js b/spec/frontend/create_cluster/init_create_cluster_spec.js
index 1fdcb57492d..42d1ceed864 100644
--- a/spec/frontend/create_cluster/init_create_cluster_spec.js
+++ b/spec/frontend/create_cluster/init_create_cluster_spec.js
@@ -1,6 +1,6 @@
-import initCreateCluster from '~/create_cluster/init_create_cluster';
import initGkeDropdowns from '~/create_cluster/gke_cluster';
import initGkeNamespace from '~/create_cluster/gke_cluster_namespace';
+import initCreateCluster from '~/create_cluster/init_create_cluster';
import PersistentUserCallout from '~/persistent_user_callout';
// This import is loaded dynamically in `init_create_cluster`.
diff --git a/spec/frontend/create_cluster/store/cluster_dropdown/actions_spec.js b/spec/frontend/create_cluster/store/cluster_dropdown/actions_spec.js
index 014b527161f..c0e8b11cf1e 100644
--- a/spec/frontend/create_cluster/store/cluster_dropdown/actions_spec.js
+++ b/spec/frontend/create_cluster/store/cluster_dropdown/actions_spec.js
@@ -1,8 +1,8 @@
import testAction from 'helpers/vuex_action_helper';
-import createState from '~/create_cluster/store/cluster_dropdown/state';
-import * as types from '~/create_cluster/store/cluster_dropdown/mutation_types';
import actionsFactory from '~/create_cluster/store/cluster_dropdown/actions';
+import * as types from '~/create_cluster/store/cluster_dropdown/mutation_types';
+import createState from '~/create_cluster/store/cluster_dropdown/state';
describe('Cluster dropdown Store Actions', () => {
const items = [{ name: 'item 1' }];
diff --git a/spec/frontend/create_cluster/store/cluster_dropdown/mutations_spec.js b/spec/frontend/create_cluster/store/cluster_dropdown/mutations_spec.js
index 4b700e31675..197fcfc2600 100644
--- a/spec/frontend/create_cluster/store/cluster_dropdown/mutations_spec.js
+++ b/spec/frontend/create_cluster/store/cluster_dropdown/mutations_spec.js
@@ -3,8 +3,8 @@ import {
RECEIVE_ITEMS_SUCCESS,
RECEIVE_ITEMS_ERROR,
} from '~/create_cluster/store/cluster_dropdown/mutation_types';
-import createState from '~/create_cluster/store/cluster_dropdown/state';
import mutations from '~/create_cluster/store/cluster_dropdown/mutations';
+import createState from '~/create_cluster/store/cluster_dropdown/state';
describe('Cluster dropdown store mutations', () => {
let state;
diff --git a/spec/frontend/create_merge_request_dropdown_spec.js b/spec/frontend/create_merge_request_dropdown_spec.js
index 698725b769d..08c05c6ec38 100644
--- a/spec/frontend/create_merge_request_dropdown_spec.js
+++ b/spec/frontend/create_merge_request_dropdown_spec.js
@@ -1,8 +1,8 @@
import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
-import axios from '~/lib/utils/axios_utils';
-import CreateMergeRequestDropdown from '~/create_merge_request_dropdown';
import confidentialState from '~/confidential_merge_request/state';
+import CreateMergeRequestDropdown from '~/create_merge_request_dropdown';
+import axios from '~/lib/utils/axios_utils';
describe('CreateMergeRequestDropdown', () => {
let axiosMock;
diff --git a/spec/frontend/cycle_analytics/limit_warning_component_spec.js b/spec/frontend/cycle_analytics/limit_warning_component_spec.js
index edde3725dd6..3dac7438909 100644
--- a/spec/frontend/cycle_analytics/limit_warning_component_spec.js
+++ b/spec/frontend/cycle_analytics/limit_warning_component_spec.js
@@ -1,7 +1,7 @@
-import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
-import Translate from '~/vue_shared/translate';
+import Vue from 'vue';
import LimitWarningComponent from '~/cycle_analytics/components/limit_warning_component.vue';
+import Translate from '~/vue_shared/translate';
Vue.use(Translate);
diff --git a/spec/frontend/deploy_freeze/components/deploy_freeze_modal_spec.js b/spec/frontend/deploy_freeze/components/deploy_freeze_modal_spec.js
index 650825d1cb7..d8ce184940a 100644
--- a/spec/frontend/deploy_freeze/components/deploy_freeze_modal_spec.js
+++ b/spec/frontend/deploy_freeze/components/deploy_freeze_modal_spec.js
@@ -1,9 +1,9 @@
-import Vuex from 'vuex';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlButton, GlModal } from '@gitlab/ui';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import Vuex from 'vuex';
import DeployFreezeModal from '~/deploy_freeze/components/deploy_freeze_modal.vue';
-import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown.vue';
import createStore from '~/deploy_freeze/store';
+import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown.vue';
import { freezePeriodsFixture, timezoneDataFixture } from '../helpers';
const localVue = createLocalVue();
diff --git a/spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js b/spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js
index c29a0c0ca73..392652292cf 100644
--- a/spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js
+++ b/spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js
@@ -1,8 +1,8 @@
-import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
+import Vuex from 'vuex';
+import DeployFreezeModal from '~/deploy_freeze/components/deploy_freeze_modal.vue';
import DeployFreezeSettings from '~/deploy_freeze/components/deploy_freeze_settings.vue';
import DeployFreezeTable from '~/deploy_freeze/components/deploy_freeze_table.vue';
-import DeployFreezeModal from '~/deploy_freeze/components/deploy_freeze_modal.vue';
import createStore from '~/deploy_freeze/store';
import { timezoneDataFixture } from '../helpers';
diff --git a/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js b/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js
index 3b8e8f8485e..e4ee1b9ad26 100644
--- a/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js
+++ b/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js
@@ -1,5 +1,5 @@
-import Vuex from 'vuex';
import { createLocalVue, mount } from '@vue/test-utils';
+import Vuex from 'vuex';
import DeployFreezeTable from '~/deploy_freeze/components/deploy_freeze_table.vue';
import createStore from '~/deploy_freeze/store';
import { freezePeriodsFixture, timezoneDataFixture } from '../helpers';
diff --git a/spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js b/spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js
index 1b03cc03d02..4dd5c29a917 100644
--- a/spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js
+++ b/spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js
@@ -1,8 +1,8 @@
-import Vuex from 'vuex';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlDropdownItem, GlDropdown } from '@gitlab/ui';
-import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown.vue';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
import createStore from '~/deploy_freeze/store';
+import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown.vue';
import { findTzByName, formatTz, timezoneDataFixture } from '../helpers';
const localVue = createLocalVue();
diff --git a/spec/frontend/deploy_freeze/store/actions_spec.js b/spec/frontend/deploy_freeze/store/actions_spec.js
index 3c9d25c4f5c..f4d9802e39a 100644
--- a/spec/frontend/deploy_freeze/store/actions_spec.js
+++ b/spec/frontend/deploy_freeze/store/actions_spec.js
@@ -1,11 +1,11 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
-import axios from '~/lib/utils/axios_utils';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
-import getInitialState from '~/deploy_freeze/store/state';
import * as actions from '~/deploy_freeze/store/actions';
import * as types from '~/deploy_freeze/store/mutation_types';
+import getInitialState from '~/deploy_freeze/store/state';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
import { freezePeriodsFixture, timezoneDataFixture } from '../helpers';
jest.mock('~/api.js');
diff --git a/spec/frontend/deploy_freeze/store/mutations_spec.js b/spec/frontend/deploy_freeze/store/mutations_spec.js
index 7cb208f16b2..54cbdfcb64c 100644
--- a/spec/frontend/deploy_freeze/store/mutations_spec.js
+++ b/spec/frontend/deploy_freeze/store/mutations_spec.js
@@ -1,6 +1,6 @@
-import state from '~/deploy_freeze/store/state';
-import mutations from '~/deploy_freeze/store/mutations';
import * as types from '~/deploy_freeze/store/mutation_types';
+import mutations from '~/deploy_freeze/store/mutations';
+import state from '~/deploy_freeze/store/state';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { findTzByName, formatTz, freezePeriodsFixture, timezoneDataFixture } from '../helpers';
diff --git a/spec/frontend/deploy_keys/components/action_btn_spec.js b/spec/frontend/deploy_keys/components/action_btn_spec.js
index b8211b02464..21281ff15b1 100644
--- a/spec/frontend/deploy_keys/components/action_btn_spec.js
+++ b/spec/frontend/deploy_keys/components/action_btn_spec.js
@@ -1,7 +1,7 @@
-import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
-import eventHub from '~/deploy_keys/eventhub';
+import { shallowMount } from '@vue/test-utils';
import actionBtn from '~/deploy_keys/components/action_btn.vue';
+import eventHub from '~/deploy_keys/eventhub';
describe('Deploy keys action btn', () => {
const data = getJSONFixture('deploy_keys/keys.json');
diff --git a/spec/frontend/deploy_keys/components/app_spec.js b/spec/frontend/deploy_keys/components/app_spec.js
index 479320f92f2..b48e0424580 100644
--- a/spec/frontend/deploy_keys/components/app_spec.js
+++ b/spec/frontend/deploy_keys/components/app_spec.js
@@ -1,10 +1,10 @@
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import { TEST_HOST } from 'spec/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
-import axios from '~/lib/utils/axios_utils';
-import eventHub from '~/deploy_keys/eventhub';
+import { TEST_HOST } from 'spec/test_constants';
import deployKeysApp from '~/deploy_keys/components/app.vue';
+import eventHub from '~/deploy_keys/eventhub';
+import axios from '~/lib/utils/axios_utils';
const TEST_ENDPOINT = `${TEST_HOST}/dummy/`;
diff --git a/spec/frontend/deploy_keys/components/key_spec.js b/spec/frontend/deploy_keys/components/key_spec.js
index fcb4e31dec8..5420f9a01f9 100644
--- a/spec/frontend/deploy_keys/components/key_spec.js
+++ b/spec/frontend/deploy_keys/components/key_spec.js
@@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
-import DeployKeysStore from '~/deploy_keys/store';
import key from '~/deploy_keys/components/key.vue';
+import DeployKeysStore from '~/deploy_keys/store';
import { getTimeago } from '~/lib/utils/datetime_utility';
describe('Deploy keys key', () => {
@@ -76,7 +76,7 @@ describe('Deploy keys key', () => {
createComponent({ deployKey: { ...deployKey, deploy_keys_projects: deployKeysProjects } });
expect(wrapper.find('.deploy-project-label').attributes('title')).toBe(
- 'Write access allowed',
+ 'Grant write permissions to this key',
);
});
diff --git a/spec/frontend/deploy_keys/components/keys_panel_spec.js b/spec/frontend/deploy_keys/components/keys_panel_spec.js
index 34b592017e9..d6419356166 100644
--- a/spec/frontend/deploy_keys/components/keys_panel_spec.js
+++ b/spec/frontend/deploy_keys/components/keys_panel_spec.js
@@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
-import DeployKeysStore from '~/deploy_keys/store';
import deployKeysPanel from '~/deploy_keys/components/keys_panel.vue';
+import DeployKeysStore from '~/deploy_keys/store';
describe('Deploy keys panel', () => {
const data = getJSONFixture('deploy_keys/keys.json');
diff --git a/spec/frontend/design_management/components/delete_button_spec.js b/spec/frontend/design_management/components/delete_button_spec.js
index 961f5bdd2ae..8f7d8e0b214 100644
--- a/spec/frontend/design_management/components/delete_button_spec.js
+++ b/spec/frontend/design_management/components/delete_button_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import BatchDeleteButton from '~/design_management/components/delete_button.vue';
describe('Batch delete button component', () => {
diff --git a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
index 77fc70e08d1..92e188f4bcc 100644
--- a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
@@ -1,14 +1,14 @@
-import { mount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
-import notes from '../../mock_data/notes';
+import { mount } from '@vue/test-utils';
import DesignDiscussion from '~/design_management/components/design_notes/design_discussion.vue';
import DesignNote from '~/design_management/components/design_notes/design_note.vue';
import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue';
+import ToggleRepliesWidget from '~/design_management/components/design_notes/toggle_replies_widget.vue';
import createNoteMutation from '~/design_management/graphql/mutations/create_note.mutation.graphql';
import toggleResolveDiscussionMutation from '~/design_management/graphql/mutations/toggle_resolve_discussion.mutation.graphql';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
-import ToggleRepliesWidget from '~/design_management/components/design_notes/toggle_replies_widget.vue';
import mockDiscussion from '../../mock_data/discussion';
+import notes from '../../mock_data/notes';
const defaultMockDiscussion = {
id: '0',
diff --git a/spec/frontend/design_management/components/design_notes/design_note_spec.js b/spec/frontend/design_management/components/design_notes/design_note_spec.js
index 043091e3dc2..1cd556eabb4 100644
--- a/spec/frontend/design_management/components/design_notes/design_note_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_note_spec.js
@@ -1,9 +1,9 @@
import { shallowMount } from '@vue/test-utils';
import { ApolloMutation } from 'vue-apollo';
import DesignNote from '~/design_management/components/design_notes/design_note.vue';
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
-import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
const scrollIntoViewMock = jest.fn();
const note = {
diff --git a/spec/frontend/design_management/components/design_notes/toggle_replies_widget_spec.js b/spec/frontend/design_management/components/design_notes/toggle_replies_widget_spec.js
index 7eda294d2d3..f87228663b6 100644
--- a/spec/frontend/design_management/components/design_notes/toggle_replies_widget_spec.js
+++ b/spec/frontend/design_management/components/design_notes/toggle_replies_widget_spec.js
@@ -1,7 +1,7 @@
-import { shallowMount } from '@vue/test-utils';
import { GlIcon, GlButton, GlLink } from '@gitlab/ui';
-import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import { shallowMount } from '@vue/test-utils';
import ToggleRepliesWidget from '~/design_management/components/design_notes/toggle_replies_widget.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import notes from '../../mock_data/notes';
describe('Toggle replies widget component', () => {
diff --git a/spec/frontend/design_management/components/design_overlay_spec.js b/spec/frontend/design_management/components/design_overlay_spec.js
index a026cc39c84..d3119be7159 100644
--- a/spec/frontend/design_management/components/design_overlay_spec.js
+++ b/spec/frontend/design_management/components/design_overlay_spec.js
@@ -1,8 +1,8 @@
import { mount } from '@vue/test-utils';
import DesignOverlay from '~/design_management/components/design_overlay.vue';
+import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '~/design_management/constants';
import updateActiveDiscussion from '~/design_management/graphql/mutations/update_active_discussion.mutation.graphql';
import notes from '../mock_data/notes';
-import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '~/design_management/constants';
const mutate = jest.fn(() => Promise.resolve());
diff --git a/spec/frontend/design_management/components/design_presentation_spec.js b/spec/frontend/design_management/components/design_presentation_spec.js
index 31fd154dc4b..edf8b965153 100644
--- a/spec/frontend/design_management/components/design_presentation_spec.js
+++ b/spec/frontend/design_management/components/design_presentation_spec.js
@@ -1,7 +1,7 @@
-import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
-import DesignPresentation from '~/design_management/components/design_presentation.vue';
+import { nextTick } from 'vue';
import DesignOverlay from '~/design_management/components/design_overlay.vue';
+import DesignPresentation from '~/design_management/components/design_presentation.vue';
const mockOverlayData = {
overlayDimensions: {
diff --git a/spec/frontend/design_management/components/design_scaler_spec.js b/spec/frontend/design_management/components/design_scaler_spec.js
index 40f53e8d0bf..8a123b2d1e5 100644
--- a/spec/frontend/design_management/components/design_scaler_spec.js
+++ b/spec/frontend/design_management/components/design_scaler_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import DesignScaler from '~/design_management/components/design_scaler.vue';
describe('Design management design scaler component', () => {
diff --git a/spec/frontend/design_management/components/design_sidebar_spec.js b/spec/frontend/design_management/components/design_sidebar_spec.js
index 60266883fcd..8eb993ec7b5 100644
--- a/spec/frontend/design_management/components/design_sidebar_spec.js
+++ b/spec/frontend/design_management/components/design_sidebar_spec.js
@@ -1,12 +1,12 @@
-import { shallowMount } from '@vue/test-utils';
import { GlCollapse, GlPopover } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import Cookies from 'js-cookie';
+import DesignDiscussion from '~/design_management/components/design_notes/design_discussion.vue';
import DesignSidebar from '~/design_management/components/design_sidebar.vue';
+import DesignTodoButton from '~/design_management/components/design_todo_button.vue';
+import updateActiveDiscussionMutation from '~/design_management/graphql/mutations/update_active_discussion.mutation.graphql';
import Participants from '~/sidebar/components/participants/participants.vue';
-import DesignDiscussion from '~/design_management/components/design_notes/design_discussion.vue';
import design from '../mock_data/design';
-import updateActiveDiscussionMutation from '~/design_management/graphql/mutations/update_active_discussion.mutation.graphql';
-import DesignTodoButton from '~/design_management/components/design_todo_button.vue';
const scrollIntoViewMock = jest.fn();
HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
diff --git a/spec/frontend/design_management/components/design_todo_button_spec.js b/spec/frontend/design_management/components/design_todo_button_spec.js
index 9ebc6ca26a2..20686d0ae6c 100644
--- a/spec/frontend/design_management/components/design_todo_button_spec.js
+++ b/spec/frontend/design_management/components/design_todo_button_spec.js
@@ -1,8 +1,8 @@
import { shallowMount, mount } from '@vue/test-utils';
-import TodoButton from '~/vue_shared/components/todo_button.vue';
import DesignTodoButton from '~/design_management/components/design_todo_button.vue';
import createDesignTodoMutation from '~/design_management/graphql/mutations/create_design_todo.mutation.graphql';
import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql';
+import TodoButton from '~/vue_shared/components/todo_button.vue';
import mockDesign from '../mock_data/design';
const mockDesignWithPendingTodos = {
@@ -111,7 +111,7 @@ describe('Design management design todo button', () => {
});
it('renders correct button text', () => {
- expect(wrapper.text()).toBe('Add a To Do');
+ expect(wrapper.text()).toBe('Add a to do');
});
describe('when clicked', () => {
diff --git a/spec/frontend/design_management/components/image_spec.js b/spec/frontend/design_management/components/image_spec.js
index 52d60b04a8a..765d902f9a6 100644
--- a/spec/frontend/design_management/components/image_spec.js
+++ b/spec/frontend/design_management/components/image_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import DesignImage from '~/design_management/components/image.vue';
describe('Design management large image component', () => {
diff --git a/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap b/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap
index 36a2ffd19c3..8fe3e92360a 100644
--- a/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap
+++ b/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap
@@ -26,9 +26,10 @@ exports[`Design management list item component with notes renders item with mult
<img
alt="test"
- class="gl-display-block gl-mx-auto gl-max-w-full mh-100 design-img"
+ class="gl-display-block gl-mx-auto gl-max-w-full gl-max-h-full design-img"
data-qa-filename="test"
data-qa-selector="design_image"
+ data-testid="design-img-1"
src=""
/>
</gl-intersection-observer-stub>
@@ -43,6 +44,8 @@ exports[`Design management list item component with notes renders item with mult
<span
class="gl-font-weight-bold str-truncated-100"
data-qa-selector="design_file_name"
+ data-testid="design-img-filename-1"
+ title="test"
>
test
</span>
@@ -100,9 +103,10 @@ exports[`Design management list item component with notes renders item with sing
<img
alt="test"
- class="gl-display-block gl-mx-auto gl-max-w-full mh-100 design-img"
+ class="gl-display-block gl-mx-auto gl-max-w-full gl-max-h-full design-img"
data-qa-filename="test"
data-qa-selector="design_image"
+ data-testid="design-img-1"
src=""
/>
</gl-intersection-observer-stub>
@@ -117,6 +121,8 @@ exports[`Design management list item component with notes renders item with sing
<span
class="gl-font-weight-bold str-truncated-100"
data-qa-selector="design_file_name"
+ data-testid="design-img-filename-1"
+ title="test"
>
test
</span>
diff --git a/spec/frontend/design_management/components/list/item_spec.js b/spec/frontend/design_management/components/list/item_spec.js
index 55c6ecbc26b..caf0f8bb5bc 100644
--- a/spec/frontend/design_management/components/list/item_spec.js
+++ b/spec/frontend/design_management/components/list/item_spec.js
@@ -1,6 +1,7 @@
-import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlIcon, GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueRouter from 'vue-router';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import Item from '~/design_management/components/list/item.vue';
const localVue = createLocalVue();
@@ -17,8 +18,11 @@ const DESIGN_VERSION_EVENT = {
describe('Design management list item component', () => {
let wrapper;
+ const imgId = 1;
+ const imgFilename = 'test';
- const findDesignEvent = () => wrapper.find('[data-testid="designEvent"]');
+ const findDesignEvent = () => wrapper.findByTestId('design-event');
+ const findImgFilename = (id = imgId) => wrapper.findByTestId(`design-img-filename-${id}`);
const findEventIcon = () => findDesignEvent().find(GlIcon);
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
@@ -28,25 +32,27 @@ describe('Design management list item component', () => {
isUploading = false,
imageLoading = false,
} = {}) {
- wrapper = shallowMount(Item, {
- localVue,
- router,
- propsData: {
- id: 1,
- filename: 'test',
- image: 'http://via.placeholder.com/300',
- isUploading,
- event,
- notesCount,
- updatedAt: '01-01-2019',
- },
- data() {
- return {
- imageLoading,
- };
- },
- stubs: ['router-link'],
- });
+ wrapper = extendedWrapper(
+ shallowMount(Item, {
+ localVue,
+ router,
+ propsData: {
+ id: imgId,
+ filename: imgFilename,
+ image: 'http://via.placeholder.com/300',
+ isUploading,
+ event,
+ notesCount,
+ updatedAt: '01-01-2019',
+ },
+ data() {
+ return {
+ imageLoading,
+ };
+ },
+ stubs: ['router-link'],
+ }),
+ );
}
afterEach(() => {
@@ -75,6 +81,10 @@ describe('Design management list item component', () => {
return wrapper.vm.$nextTick();
});
+ it('renders a tooltip', () => {
+ expect(findImgFilename().attributes('title')).toEqual(imgFilename);
+ });
+
describe('before image is loaded', () => {
it('renders loading spinner', () => {
expect(wrapper.find(GlLoadingIcon)).toExist();
diff --git a/spec/frontend/design_management/components/toolbar/index_spec.js b/spec/frontend/design_management/components/toolbar/index_spec.js
index 6ac088a2c53..44c865d976d 100644
--- a/spec/frontend/design_management/components/toolbar/index_spec.js
+++ b/spec/frontend/design_management/components/toolbar/index_spec.js
@@ -1,8 +1,8 @@
+import { GlButton } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueRouter from 'vue-router';
-import { GlButton } from '@gitlab/ui';
-import Toolbar from '~/design_management/components/toolbar/index.vue';
import DeleteButton from '~/design_management/components/delete_button.vue';
+import Toolbar from '~/design_management/components/toolbar/index.vue';
import { DESIGNS_ROUTE_NAME } from '~/design_management/router/constants';
const localVue = createLocalVue();
diff --git a/spec/frontend/design_management/components/upload/button_spec.js b/spec/frontend/design_management/components/upload/button_spec.js
index ea738496ad6..d123db43ce6 100644
--- a/spec/frontend/design_management/components/upload/button_spec.js
+++ b/spec/frontend/design_management/components/upload/button_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import UploadButton from '~/design_management/components/upload/button.vue';
describe('Design management upload button component', () => {
diff --git a/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js b/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js
index d841aabf2f3..1b01a363688 100644
--- a/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js
+++ b/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlDropdown, GlDropdownItem, GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import DesignVersionDropdown from '~/design_management/components/upload/design_version_dropdown.vue';
import mockAllVersions from './mock_data/all_versions';
diff --git a/spec/frontend/design_management/pages/design/index_spec.js b/spec/frontend/design_management/pages/design/index_spec.js
index 9c11af28cf0..11c88c3d0f5 100644
--- a/spec/frontend/design_management/pages/design/index_spec.js
+++ b/spec/frontend/design_management/pages/design/index_spec.js
@@ -1,32 +1,32 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import VueRouter from 'vue-router';
import { GlAlert } from '@gitlab/ui';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
import { ApolloMutation } from 'vue-apollo';
+import VueRouter from 'vue-router';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
-import createFlash from '~/flash';
import Api from '~/api';
-import DesignIndex from '~/design_management/pages/design/index.vue';
-import DesignSidebar from '~/design_management/components/design_sidebar.vue';
import DesignPresentation from '~/design_management/components/design_presentation.vue';
+import DesignSidebar from '~/design_management/components/design_sidebar.vue';
+import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '~/design_management/constants';
import createImageDiffNoteMutation from '~/design_management/graphql/mutations/create_image_diff_note.mutation.graphql';
import updateActiveDiscussion from '~/design_management/graphql/mutations/update_active_discussion.mutation.graphql';
+import DesignIndex from '~/design_management/pages/design/index.vue';
+import createRouter from '~/design_management/router';
+import { DESIGNS_ROUTE_NAME, DESIGN_ROUTE_NAME } from '~/design_management/router/constants';
+import * as utils from '~/design_management/utils/design_management_utils';
import {
DESIGN_NOT_FOUND_ERROR,
DESIGN_VERSION_NOT_EXIST_ERROR,
} from '~/design_management/utils/error_messages';
-import { DESIGNS_ROUTE_NAME, DESIGN_ROUTE_NAME } from '~/design_management/router/constants';
-import createRouter from '~/design_management/router';
-import * as utils from '~/design_management/utils/design_management_utils';
-import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '~/design_management/constants';
-import design from '../../mock_data/design';
-import mockResponseWithDesigns from '../../mock_data/designs';
-import mockResponseNoDesigns from '../../mock_data/no_designs';
-import mockAllVersions from '../../mock_data/all_versions';
import {
DESIGN_TRACKING_PAGE_NAME,
DESIGN_SNOWPLOW_EVENT_TYPES,
DESIGN_USAGE_PING_EVENT_TYPES,
} from '~/design_management/utils/tracking';
+import createFlash from '~/flash';
+import mockAllVersions from '../../mock_data/all_versions';
+import design from '../../mock_data/design';
+import mockResponseWithDesigns from '../../mock_data/designs';
+import mockResponseNoDesigns from '../../mock_data/no_designs';
jest.mock('~/flash');
jest.mock('~/api.js');
diff --git a/spec/frontend/design_management/pages/index_spec.js b/spec/frontend/design_management/pages/index_spec.js
index 7d28d6f6d11..4f162ca8e7f 100644
--- a/spec/frontend/design_management/pages/index_spec.js
+++ b/spec/frontend/design_management/pages/index_spec.js
@@ -1,27 +1,32 @@
-import { nextTick } from 'vue';
+import { GlEmptyState } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import VueApollo, { ApolloMutation } from 'vue-apollo';
-import VueDraggable from 'vuedraggable';
import VueRouter from 'vue-router';
-import { GlEmptyState } from '@gitlab/ui';
+import VueDraggable from 'vuedraggable';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
-import getDesignListQuery from 'shared_queries/design_management/get_design_list.query.graphql';
import permissionsQuery from 'shared_queries/design_management/design_permissions.query.graphql';
-import Index from '~/design_management/pages/index.vue';
-import uploadDesignMutation from '~/design_management/graphql/mutations/upload_design.mutation.graphql';
-import DesignDestroyer from '~/design_management/components/design_destroyer.vue';
-import DesignDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
+import getDesignListQuery from 'shared_queries/design_management/get_design_list.query.graphql';
import DeleteButton from '~/design_management/components/delete_button.vue';
+import DesignDestroyer from '~/design_management/components/design_destroyer.vue';
import Design from '~/design_management/components/list/item.vue';
+import moveDesignMutation from '~/design_management/graphql/mutations/move_design.mutation.graphql';
+import uploadDesignMutation from '~/design_management/graphql/mutations/upload_design.mutation.graphql';
+import Index from '~/design_management/pages/index.vue';
+import createRouter from '~/design_management/router';
import { DESIGNS_ROUTE_NAME } from '~/design_management/router/constants';
+import * as utils from '~/design_management/utils/design_management_utils';
import {
EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE,
EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE,
} from '~/design_management/utils/error_messages';
+import {
+ DESIGN_TRACKING_PAGE_NAME,
+ DESIGN_SNOWPLOW_EVENT_TYPES,
+} from '~/design_management/utils/tracking';
import createFlash from '~/flash';
-import createRouter from '~/design_management/router';
-import * as utils from '~/design_management/utils/design_management_utils';
+import DesignDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
import {
designListQueryResponse,
designUploadMutationCreatedResponse,
@@ -31,11 +36,6 @@ import {
reorderedDesigns,
moveDesignMutationResponseWithErrors,
} from '../mock_data/apollo_mock';
-import moveDesignMutation from '~/design_management/graphql/mutations/move_design.mutation.graphql';
-import {
- DESIGN_TRACKING_PAGE_NAME,
- DESIGN_SNOWPLOW_EVENT_TYPES,
-} from '~/design_management/utils/tracking';
jest.mock('~/flash.js');
const mockPageEl = {
diff --git a/spec/frontend/design_management/router_spec.js b/spec/frontend/design_management/router_spec.js
index 0b4e68eea78..ac5e6895408 100644
--- a/spec/frontend/design_management/router_spec.js
+++ b/spec/frontend/design_management/router_spec.js
@@ -2,8 +2,8 @@ import { mount, createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue';
import VueRouter from 'vue-router';
import App from '~/design_management/components/app.vue';
-import Designs from '~/design_management/pages/index.vue';
import DesignDetail from '~/design_management/pages/design/index.vue';
+import Designs from '~/design_management/pages/index.vue';
import createRouter from '~/design_management/router';
import { DESIGNS_ROUTE_NAME, DESIGN_ROUTE_NAME } from '~/design_management/router/constants';
import '~/commons/bootstrap';
diff --git a/spec/frontend/design_management/utils/cache_update_spec.js b/spec/frontend/design_management/utils/cache_update_spec.js
index 2fb08c3ef05..7327cf00abd 100644
--- a/spec/frontend/design_management/utils/cache_update_spec.js
+++ b/spec/frontend/design_management/utils/cache_update_spec.js
@@ -10,8 +10,8 @@ import {
ADD_IMAGE_DIFF_NOTE_ERROR,
UPDATE_IMAGE_DIFF_NOTE_ERROR,
} from '~/design_management/utils/error_messages';
-import design from '../mock_data/design';
import createFlash from '~/flash';
+import design from '../mock_data/design';
jest.mock('~/flash.js');
diff --git a/spec/frontend/design_management/utils/design_management_utils_spec.js b/spec/frontend/design_management/utils/design_management_utils_spec.js
index 368448ead10..5b7f99e9d96 100644
--- a/spec/frontend/design_management/utils/design_management_utils_spec.js
+++ b/spec/frontend/design_management/utils/design_management_utils_spec.js
@@ -8,9 +8,9 @@ import {
extractDesign,
extractDesignNoteId,
} from '~/design_management/utils/design_management_utils';
-import mockResponseNoDesigns from '../mock_data/no_designs';
-import mockResponseWithDesigns from '../mock_data/designs';
import mockDesign from '../mock_data/design';
+import mockResponseWithDesigns from '../mock_data/designs';
+import mockResponseNoDesigns from '../mock_data/no_designs';
jest.mock('lodash/uniqueId', () => () => 1);
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index 7fbeb33dd93..d2b5338a0cc 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -1,26 +1,26 @@
-import Vue, { nextTick } from 'vue';
-import Vuex from 'vuex';
-import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon, GlPagination } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import { TEST_HOST } from 'spec/test_constants';
import Mousetrap from 'mousetrap';
+import Vue, { nextTick } from 'vue';
+import Vuex from 'vuex';
+import { TEST_HOST } from 'spec/test_constants';
import App from '~/diffs/components/app.vue';
-import NoChanges from '~/diffs/components/no_changes.vue';
-import DiffFile from '~/diffs/components/diff_file.vue';
-import CompareVersions from '~/diffs/components/compare_versions.vue';
-import HiddenFilesWarning from '~/diffs/components/hidden_files_warning.vue';
import CollapsedFilesWarning from '~/diffs/components/collapsed_files_warning.vue';
import CommitWidget from '~/diffs/components/commit_widget.vue';
+import CompareVersions from '~/diffs/components/compare_versions.vue';
+import DiffFile from '~/diffs/components/diff_file.vue';
+import HiddenFilesWarning from '~/diffs/components/hidden_files_warning.vue';
+import NoChanges from '~/diffs/components/no_changes.vue';
import TreeList from '~/diffs/components/tree_list.vue';
-import createDiffsStore from '../create_diffs_store';
-import axios from '~/lib/utils/axios_utils';
-import * as urlUtils from '~/lib/utils/url_utility';
-import diffsMockData from '../mock_data/merge_request_diffs';
import { EVT_VIEW_FILE_BY_FILE } from '~/diffs/constants';
import eventHub from '~/diffs/event_hub';
+import axios from '~/lib/utils/axios_utils';
+import * as urlUtils from '~/lib/utils/url_utility';
+import createDiffsStore from '../create_diffs_store';
+import diffsMockData from '../mock_data/merge_request_diffs';
const mergeRequestDiff = { version_index: 1 };
const TEST_ENDPOINT = `${TEST_HOST}/diff/endpoint`;
diff --git a/spec/frontend/diffs/components/collapsed_files_warning_spec.js b/spec/frontend/diffs/components/collapsed_files_warning_spec.js
index 75e76d88b6b..77c2e19cb68 100644
--- a/spec/frontend/diffs/components/collapsed_files_warning_spec.js
+++ b/spec/frontend/diffs/components/collapsed_files_warning_spec.js
@@ -1,9 +1,9 @@
-import Vuex from 'vuex';
import { shallowMount, mount, createLocalVue } from '@vue/test-utils';
-import createStore from '~/diffs/store/modules';
+import Vuex from 'vuex';
import CollapsedFilesWarning from '~/diffs/components/collapsed_files_warning.vue';
import { CENTERED_LIMITED_CONTAINER_CLASSES, EVT_EXPAND_ALL_FILES } from '~/diffs/constants';
import eventHub from '~/diffs/event_hub';
+import createStore from '~/diffs/store/modules';
const propsData = {
limited: true,
diff --git a/spec/frontend/diffs/components/commit_item_spec.js b/spec/frontend/diffs/components/commit_item_spec.js
index f588f65dafd..8cb4fd20063 100644
--- a/spec/frontend/diffs/components/commit_item_spec.js
+++ b/spec/frontend/diffs/components/commit_item_spec.js
@@ -1,8 +1,8 @@
import { mount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
import { trimText } from 'helpers/text_helper';
-import { getTimeago } from '~/lib/utils/datetime_utility';
import Component from '~/diffs/components/commit_item.vue';
+import { getTimeago } from '~/lib/utils/datetime_utility';
import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
import getDiffWithCommit from '../mock_data/diff_with_commit';
diff --git a/spec/frontend/diffs/components/commit_widget_spec.js b/spec/frontend/diffs/components/commit_widget_spec.js
index 54e7596b726..fbff473e4df 100644
--- a/spec/frontend/diffs/components/commit_widget_spec.js
+++ b/spec/frontend/diffs/components/commit_widget_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import CommitWidget from '~/diffs/components/commit_widget.vue';
import CommitItem from '~/diffs/components/commit_item.vue';
+import CommitWidget from '~/diffs/components/commit_widget.vue';
describe('diffs/components/commit_widget', () => {
let wrapper;
diff --git a/spec/frontend/diffs/components/compare_dropdown_layout_spec.js b/spec/frontend/diffs/components/compare_dropdown_layout_spec.js
index d99933a1ee9..98f88226742 100644
--- a/spec/frontend/diffs/components/compare_dropdown_layout_spec.js
+++ b/spec/frontend/diffs/components/compare_dropdown_layout_spec.js
@@ -1,7 +1,7 @@
import { mount } from '@vue/test-utils';
import { trimText } from 'helpers/text_helper';
-import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import CompareDropdownLayout from '~/diffs/components/compare_dropdown_layout.vue';
+import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
const TEST_COMMIT_TEXT = '1 commit';
const TEST_CREATED_AT = '2018-10-23T11:49:16.611Z';
@@ -69,7 +69,7 @@ describe('CompareDropdownLayout', () => {
expect(findListItemsData()).toEqual([
{
href: 'version/1',
- text: 'version 1 (base) abcdef1 1 commit 2 years ago',
+ text: 'version 1 (base) abcdef1 1 commit 1 year ago',
createdAt: TEST_CREATED_AT,
isActive: true,
},
diff --git a/spec/frontend/diffs/components/compare_versions_spec.js b/spec/frontend/diffs/components/compare_versions_spec.js
index 949cc855200..c93a3771ec0 100644
--- a/spec/frontend/diffs/components/compare_versions_spec.js
+++ b/spec/frontend/diffs/components/compare_versions_spec.js
@@ -1,10 +1,10 @@
-import { trimText } from 'helpers/text_helper';
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
+import { trimText } from 'helpers/text_helper';
import CompareVersionsComponent from '~/diffs/components/compare_versions.vue';
import { createStore } from '~/mr_notes/stores';
-import diffsMockData from '../mock_data/merge_request_diffs';
import getDiffWithCommit from '../mock_data/diff_with_commit';
+import diffsMockData from '../mock_data/merge_request_diffs';
const localVue = createLocalVue();
localVue.use(Vuex);
diff --git a/spec/frontend/diffs/components/diff_comment_cell_spec.js b/spec/frontend/diffs/components/diff_comment_cell_spec.js
index d6b68fc52d7..b636a178593 100644
--- a/spec/frontend/diffs/components/diff_comment_cell_spec.js
+++ b/spec/frontend/diffs/components/diff_comment_cell_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import DiffCommentCell from '~/diffs/components/diff_comment_cell.vue';
-import DiffDiscussions from '~/diffs/components/diff_discussions.vue';
import DiffDiscussionReply from '~/diffs/components/diff_discussion_reply.vue';
+import DiffDiscussions from '~/diffs/components/diff_discussions.vue';
describe('DiffCommentCell', () => {
const createWrapper = (props = {}) => {
diff --git a/spec/frontend/diffs/components/diff_content_spec.js b/spec/frontend/diffs/components/diff_content_spec.js
index c1cf4793c88..db4d69f0176 100644
--- a/spec/frontend/diffs/components/diff_content_spec.js
+++ b/spec/frontend/diffs/components/diff_content_spec.js
@@ -1,17 +1,17 @@
+import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import { GlLoadingIcon } from '@gitlab/ui';
import DiffContentComponent from '~/diffs/components/diff_content.vue';
+import DiffDiscussions from '~/diffs/components/diff_discussions.vue';
+import DiffView from '~/diffs/components/diff_view.vue';
import InlineDiffView from '~/diffs/components/inline_diff_view.vue';
-import NotDiffableViewer from '~/vue_shared/components/diff_viewer/viewers/not_diffable.vue';
-import NoPreviewViewer from '~/vue_shared/components/diff_viewer/viewers/no_preview.vue';
import ParallelDiffView from '~/diffs/components/parallel_diff_view.vue';
-import NoteForm from '~/notes/components/note_form.vue';
-import DiffDiscussions from '~/diffs/components/diff_discussions.vue';
import { IMAGE_DIFF_POSITION_TYPE } from '~/diffs/constants';
-import diffFileMockData from '../mock_data/diff_file';
import { diffViewerModes } from '~/ide/constants';
-import DiffView from '~/diffs/components/diff_view.vue';
+import NoteForm from '~/notes/components/note_form.vue';
+import NoPreviewViewer from '~/vue_shared/components/diff_viewer/viewers/no_preview.vue';
+import NotDiffableViewer from '~/vue_shared/components/diff_viewer/viewers/not_diffable.vue';
+import diffFileMockData from '../mock_data/diff_file';
const localVue = createLocalVue();
localVue.use(Vuex);
diff --git a/spec/frontend/diffs/components/diff_discussions_spec.js b/spec/frontend/diffs/components/diff_discussions_spec.js
index 5c390054247..bd6f4cd2545 100644
--- a/spec/frontend/diffs/components/diff_discussions_spec.js
+++ b/spec/frontend/diffs/components/diff_discussions_spec.js
@@ -1,10 +1,10 @@
-import { mount, createLocalVue } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
+import { mount, createLocalVue } from '@vue/test-utils';
import DiffDiscussions from '~/diffs/components/diff_discussions.vue';
-import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
-import NoteableDiscussion from '~/notes/components/noteable_discussion.vue';
-import DiscussionNotes from '~/notes/components/discussion_notes.vue';
import { createStore } from '~/mr_notes/stores';
+import DiscussionNotes from '~/notes/components/discussion_notes.vue';
+import NoteableDiscussion from '~/notes/components/noteable_discussion.vue';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import '~/behaviors/markdown/render_gfm';
import discussionsMockData from '../mock_data/diff_discussions';
diff --git a/spec/frontend/diffs/components/diff_expansion_cell_spec.js b/spec/frontend/diffs/components/diff_expansion_cell_spec.js
index 62e85b31f76..f53f10d955d 100644
--- a/spec/frontend/diffs/components/diff_expansion_cell_spec.js
+++ b/spec/frontend/diffs/components/diff_expansion_cell_spec.js
@@ -1,10 +1,10 @@
-import { cloneDeep } from 'lodash';
-import { mount } from '@vue/test-utils';
import { getByText } from '@testing-library/dom';
-import { createStore } from '~/mr_notes/stores';
+import { mount } from '@vue/test-utils';
+import { cloneDeep } from 'lodash';
import DiffExpansionCell from '~/diffs/components/diff_expansion_cell.vue';
-import { getPreviousLineIndex } from '~/diffs/store/utils';
import { INLINE_DIFF_VIEW_TYPE } from '~/diffs/constants';
+import { getPreviousLineIndex } from '~/diffs/store/utils';
+import { createStore } from '~/mr_notes/stores';
import diffFileMockData from '../mock_data/diff_file';
const EXPAND_UP_CLASS = '.js-unfold';
diff --git a/spec/frontend/diffs/components/diff_file_header_spec.js b/spec/frontend/diffs/components/diff_file_header_spec.js
index e9a63e861ed..b16ef8fe6b0 100644
--- a/spec/frontend/diffs/components/diff_file_header_spec.js
+++ b/spec/frontend/diffs/components/diff_file_header_spec.js
@@ -1,22 +1,29 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
-import Vuex from 'vuex';
import { cloneDeep } from 'lodash';
+import Vuex from 'vuex';
import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
+import { DIFF_FILE_AUTOMATIC_COLLAPSE, DIFF_FILE_MANUAL_COLLAPSE } from '~/diffs/constants';
+import { reviewFile } from '~/diffs/store/actions';
+import { SET_MR_FILE_REVIEWS } from '~/diffs/store/mutation_types';
+import { diffViewerModes } from '~/ide/constants';
+import { scrollToElement } from '~/lib/utils/common_utils';
+import { truncateSha } from '~/lib/utils/text_utility';
+import { __, sprintf } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
+
+import testAction from '../../__helpers__/vuex_action_helper';
import diffDiscussionsMockData from '../mock_data/diff_discussions';
-import { truncateSha } from '~/lib/utils/text_utility';
-import { diffViewerModes } from '~/ide/constants';
-import { __, sprintf } from '~/locale';
-import { scrollToElement } from '~/lib/utils/common_utils';
jest.mock('~/lib/utils/common_utils');
const diffFile = Object.freeze(
Object.assign(diffDiscussionsMockData.diff_file, {
+ id: '123',
+ file_identifier_hash: 'abc',
edit_path: 'link:/to/edit/path',
blob: {
id: '848ed9407c6730ff16edb3dd24485a0eea24292a',
@@ -52,6 +59,8 @@ describe('DiffFileHeader component', () => {
toggleFileDiscussionWrappers: jest.fn(),
toggleFullDiff: jest.fn(),
toggleActiveFileByHash: jest.fn(),
+ setFileCollapsedByUser: jest.fn(),
+ reviewFile: jest.fn(),
},
},
},
@@ -79,10 +88,11 @@ describe('DiffFileHeader component', () => {
const findViewFileButton = () => wrapper.find({ ref: 'viewButton' });
const findCollapseIcon = () => wrapper.find({ ref: 'collapseIcon' });
const findEditButton = () => wrapper.find({ ref: 'editButton' });
+ const findReviewFileCheckbox = () => wrapper.find("[data-testid='fileReviewCheckbox']");
- const createComponent = (props) => {
+ const createComponent = ({ props, options = {} } = {}) => {
mockStoreConfig = cloneDeep(defaultMockStoreConfig);
- const store = new Vuex.Store(mockStoreConfig);
+ const store = new Vuex.Store({ ...mockStoreConfig, ...(options.store || {}) });
wrapper = shallowMount(DiffFileHeader, {
propsData: {
@@ -91,6 +101,7 @@ describe('DiffFileHeader component', () => {
viewDiffsFileByFile: false,
...props,
},
+ ...options,
localVue,
store,
});
@@ -101,7 +112,7 @@ describe('DiffFileHeader component', () => {
${'visible'} | ${true}
${'hidden'} | ${false}
`('collapse toggle is $visibility if collapsible is $collapsible', ({ collapsible }) => {
- createComponent({ collapsible });
+ createComponent({ props: { collapsible } });
expect(findCollapseIcon().exists()).toBe(collapsible);
});
@@ -110,7 +121,7 @@ describe('DiffFileHeader component', () => {
${true} | ${'chevron-down'}
${false} | ${'chevron-right'}
`('collapse icon is $icon if expanded is $expanded', ({ icon, expanded }) => {
- createComponent({ expanded, collapsible: true });
+ createComponent({ props: { expanded, collapsible: true } });
expect(findCollapseIcon().props('name')).toBe(icon);
});
@@ -124,7 +135,7 @@ describe('DiffFileHeader component', () => {
});
it('when collapseIcon is clicked emits toggleFile', () => {
- createComponent({ collapsible: true });
+ createComponent({ props: { collapsible: true } });
findCollapseIcon().vm.$emit('click', new Event('click'));
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted().toggleFile).toBeDefined();
@@ -132,7 +143,7 @@ describe('DiffFileHeader component', () => {
});
it('when other element in header is clicked does not emits toggleFile', () => {
- createComponent({ collapsible: true });
+ createComponent({ props: { collapsible: true } });
findTitleLink().trigger('click');
return wrapper.vm.$nextTick().then(() => {
@@ -171,10 +182,12 @@ describe('DiffFileHeader component', () => {
it('prefers submodule_tree_url over submodule_link for href', () => {
const submoduleTreeUrl = 'some://tree/url';
createComponent({
- discussionLink: 'discussionLink',
- diffFile: {
- ...submoduleDiffFile,
- submodule_tree_url: 'some://tree/url',
+ props: {
+ discussionLink: 'discussionLink',
+ diffFile: {
+ ...submoduleDiffFile,
+ submodule_tree_url: 'some://tree/url',
+ },
},
});
@@ -184,8 +197,10 @@ describe('DiffFileHeader component', () => {
it('uses submodule_link for href if submodule_tree_url does not exists', () => {
const submoduleLink = 'link://to/submodule';
createComponent({
- discussionLink: 'discussionLink',
- diffFile: submoduleDiffFile,
+ props: {
+ discussionLink: 'discussionLink',
+ diffFile: submoduleDiffFile,
+ },
});
expect(findTitleLink().attributes('href')).toBe(submoduleLink);
@@ -193,7 +208,9 @@ describe('DiffFileHeader component', () => {
it('uses file_path + SHA as link text', () => {
createComponent({
- diffFile: submoduleDiffFile,
+ props: {
+ diffFile: submoduleDiffFile,
+ },
});
expect(findTitleLink().text()).toContain(
@@ -203,15 +220,19 @@ describe('DiffFileHeader component', () => {
it('does not render file actions', () => {
createComponent({
- diffFile: submoduleDiffFile,
- addMergeRequestButtons: true,
+ props: {
+ diffFile: submoduleDiffFile,
+ addMergeRequestButtons: true,
+ },
});
expect(findFileActions().exists()).toBe(false);
});
it('renders submodule icon', () => {
createComponent({
- diffFile: submoduleDiffFile,
+ props: {
+ diffFile: submoduleDiffFile,
+ },
});
expect(wrapper.find(FileIcon).props('submodule')).toBe(true);
@@ -223,13 +244,15 @@ describe('DiffFileHeader component', () => {
it('for mode_changed file mode displays mode changes', () => {
createComponent({
- diffFile: {
- ...diffFile,
- a_mode: 'old-mode',
- b_mode: 'new-mode',
- viewer: {
- ...diffFile.viewer,
- name: diffViewerModes.mode_changed,
+ props: {
+ diffFile: {
+ ...diffFile,
+ a_mode: 'old-mode',
+ b_mode: 'new-mode',
+ viewer: {
+ ...diffFile.viewer,
+ name: diffViewerModes.mode_changed,
+ },
},
},
});
@@ -240,13 +263,15 @@ describe('DiffFileHeader component', () => {
'for %s file mode does not display mode changes',
(mode) => {
createComponent({
- diffFile: {
- ...diffFile,
- a_mode: 'old-mode',
- b_mode: 'new-mode',
- viewer: {
- ...diffFile.viewer,
- name: diffViewerModes[mode],
+ props: {
+ diffFile: {
+ ...diffFile,
+ a_mode: 'old-mode',
+ b_mode: 'new-mode',
+ viewer: {
+ ...diffFile.viewer,
+ name: diffViewerModes[mode],
+ },
},
},
});
@@ -256,32 +281,38 @@ describe('DiffFileHeader component', () => {
it('displays the LFS label for files stored in LFS', () => {
createComponent({
- diffFile: { ...diffFile, stored_externally: true, external_storage: 'lfs' },
+ props: {
+ diffFile: { ...diffFile, stored_externally: true, external_storage: 'lfs' },
+ },
});
expect(findLfsLabel().exists()).toBe(true);
});
it('does not display the LFS label for files stored in repository', () => {
createComponent({
- diffFile: { ...diffFile, stored_externally: false },
+ props: {
+ diffFile: { ...diffFile, stored_externally: false },
+ },
});
expect(findLfsLabel().exists()).toBe(false);
});
it('does not render view replaced file button if no replaced view path is present', () => {
createComponent({
- diffFile: { ...diffFile, replaced_view_path: null },
+ props: {
+ diffFile: { ...diffFile, replaced_view_path: null },
+ },
});
expect(findReplacedFileButton().exists()).toBe(false);
});
describe('when addMergeRequestButtons is false', () => {
it('does not render file actions', () => {
- createComponent({ addMergeRequestButtons: false });
+ createComponent({ props: { addMergeRequestButtons: false } });
expect(findFileActions().exists()).toBe(false);
});
it('should not render edit button', () => {
- createComponent({ addMergeRequestButtons: false });
+ createComponent({ props: { addMergeRequestButtons: false } });
expect(findEditButton().exists()).toBe(false);
});
});
@@ -290,7 +321,7 @@ describe('DiffFileHeader component', () => {
describe('without discussions', () => {
it('does not render a toggle discussions button', () => {
diffHasDiscussionsResultMock.mockReturnValue(false);
- createComponent({ addMergeRequestButtons: true });
+ createComponent({ props: { addMergeRequestButtons: true } });
expect(findToggleDiscussionsButton().exists()).toBe(false);
});
});
@@ -298,7 +329,7 @@ describe('DiffFileHeader component', () => {
describe('with discussions', () => {
it('dispatches toggleFileDiscussionWrappers when user clicks on toggle discussions button', () => {
diffHasDiscussionsResultMock.mockReturnValue(true);
- createComponent({ addMergeRequestButtons: true });
+ createComponent({ props: { addMergeRequestButtons: true } });
expect(findToggleDiscussionsButton().exists()).toBe(true);
findToggleDiscussionsButton().vm.$emit('click');
expect(
@@ -309,7 +340,9 @@ describe('DiffFileHeader component', () => {
it('should show edit button', () => {
createComponent({
- addMergeRequestButtons: true,
+ props: {
+ addMergeRequestButtons: true,
+ },
});
expect(findEditButton().exists()).toBe(true);
});
@@ -319,25 +352,27 @@ describe('DiffFileHeader component', () => {
const externalUrl = 'link://to/external';
const formattedExternalUrl = 'link://formatted';
createComponent({
- diffFile: {
- ...diffFile,
- external_url: externalUrl,
- formatted_external_url: formattedExternalUrl,
+ props: {
+ diffFile: {
+ ...diffFile,
+ external_url: externalUrl,
+ formatted_external_url: formattedExternalUrl,
+ },
+ addMergeRequestButtons: true,
},
- addMergeRequestButtons: true,
});
expect(findExternalLink().exists()).toBe(true);
});
it('is hidden by default', () => {
- createComponent({ addMergeRequestButtons: true });
+ createComponent({ props: { addMergeRequestButtons: true } });
expect(findExternalLink().exists()).toBe(false);
});
});
describe('without file blob', () => {
beforeEach(() => {
- createComponent({ diffFile: { ...diffFile, blob: false } });
+ createComponent({ props: { diffFile: { ...diffFile, blob: false } } });
});
it('should not render toggle discussions button', () => {
@@ -352,8 +387,10 @@ describe('DiffFileHeader component', () => {
it('should render correct file view button', () => {
const viewPath = 'link://view-path';
createComponent({
- diffFile: { ...diffFile, view_path: viewPath },
- addMergeRequestButtons: true,
+ props: {
+ diffFile: { ...diffFile, view_path: viewPath },
+ addMergeRequestButtons: true,
+ },
});
expect(findViewFileButton().attributes('href')).toBe(viewPath);
expect(findViewFileButton().text()).toEqual(
@@ -367,9 +404,11 @@ describe('DiffFileHeader component', () => {
describe('when diff is fully expanded', () => {
it('is not rendered', () => {
createComponent({
- diffFile: {
- ...diffFile,
- is_fully_expanded: true,
+ props: {
+ diffFile: {
+ ...diffFile,
+ is_fully_expanded: true,
+ },
},
});
expect(findExpandButton().exists()).toBe(false);
@@ -387,17 +426,17 @@ describe('DiffFileHeader component', () => {
};
it('renders expand to full file button if not showing full file already', () => {
- createComponent(fullyNotExpandedFileProps);
+ createComponent({ props: fullyNotExpandedFileProps });
expect(findExpandButton().exists()).toBe(true);
});
it('renders loading icon when loading full file', () => {
- createComponent(fullyNotExpandedFileProps);
+ createComponent({ props: fullyNotExpandedFileProps });
expect(findExpandButton().exists()).toBe(true);
});
it('toggles full diff on click', () => {
- createComponent(fullyNotExpandedFileProps);
+ createComponent({ props: fullyNotExpandedFileProps });
findExpandButton().vm.$emit('click');
expect(mockStoreConfig.modules.diffs.actions.toggleFullDiff).toHaveBeenCalled();
});
@@ -407,7 +446,9 @@ describe('DiffFileHeader component', () => {
it('uses discussionPath for link if it is defined', () => {
const discussionPath = 'link://to/discussion';
createComponent({
- discussionPath,
+ props: {
+ discussionPath,
+ },
});
expect(findTitleLink().attributes('href')).toBe(discussionPath);
});
@@ -436,21 +477,21 @@ describe('DiffFileHeader component', () => {
describe('for new file', () => {
it('displays the path', () => {
- createComponent({ diffFile: { ...diffFile, new_file: true } });
+ createComponent({ props: { diffFile: { ...diffFile, new_file: true } } });
expect(findTitleLink().text()).toBe(diffFile.file_path);
});
});
describe('for deleted file', () => {
it('displays the path', () => {
- createComponent({ diffFile: { ...diffFile, deleted_file: true } });
+ createComponent({ props: { diffFile: { ...diffFile, deleted_file: true } } });
expect(findTitleLink().text()).toBe(
sprintf(__('%{filePath} deleted'), { filePath: diffFile.file_path }, false),
);
});
it('does not show edit button', () => {
- createComponent({ diffFile: { ...diffFile, deleted_file: true } });
+ createComponent({ props: { diffFile: { ...diffFile, deleted_file: true } } });
expect(findEditButton().exists()).toBe(false);
});
});
@@ -458,11 +499,13 @@ describe('DiffFileHeader component', () => {
describe('for renamed file', () => {
it('displays old and new path if the file was renamed', () => {
createComponent({
- diffFile: {
- ...diffFile,
- renamed_file: true,
- old_path_html: 'old',
- new_path_html: 'new',
+ props: {
+ diffFile: {
+ ...diffFile,
+ renamed_file: true,
+ old_path_html: 'old',
+ new_path_html: 'new',
+ },
},
});
expect(findTitleLink().text()).toMatch(/^old.+new/s);
@@ -473,13 +516,132 @@ describe('DiffFileHeader component', () => {
it('renders view replaced file button', () => {
const replacedViewPath = 'some/path';
createComponent({
- diffFile: {
- ...diffFile,
- replaced_view_path: replacedViewPath,
+ props: {
+ diffFile: {
+ ...diffFile,
+ replaced_view_path: replacedViewPath,
+ },
+ addMergeRequestButtons: true,
},
- addMergeRequestButtons: true,
});
expect(findReplacedFileButton().exists()).toBe(true);
});
});
+
+ describe('file reviews', () => {
+ it('calls the action to set the new review', () => {
+ createComponent({
+ props: {
+ diffFile: {
+ ...diffFile,
+ viewer: {
+ ...diffFile.viewer,
+ automaticallyCollapsed: false,
+ manuallyCollapsed: null,
+ },
+ },
+ showLocalFileReviews: true,
+ addMergeRequestButtons: true,
+ },
+ });
+
+ const file = wrapper.vm.diffFile;
+
+ findReviewFileCheckbox().vm.$emit('change', true);
+
+ return testAction(
+ reviewFile,
+ { file, reviewed: true },
+ {},
+ [{ type: SET_MR_FILE_REVIEWS, payload: { [file.file_identifier_hash]: [file.id] } }],
+ [],
+ );
+ });
+
+ it.each`
+ description | newReviewedStatus | collapseType | aCollapse | mCollapse | callAction
+ ${'does nothing'} | ${true} | ${DIFF_FILE_MANUAL_COLLAPSE} | ${false} | ${true} | ${false}
+ ${'does nothing'} | ${false} | ${DIFF_FILE_AUTOMATIC_COLLAPSE} | ${true} | ${null} | ${false}
+ ${'does nothing'} | ${true} | ${'not collapsed'} | ${false} | ${null} | ${false}
+ ${'does nothing'} | ${false} | ${'not collapsed'} | ${false} | ${null} | ${false}
+ ${'collapses the file'} | ${true} | ${DIFF_FILE_AUTOMATIC_COLLAPSE} | ${true} | ${null} | ${true}
+ `(
+ "$description if the new review status is reviewed = $newReviewedStatus and the file's collapse type is collapse = $collapseType",
+ ({ newReviewedStatus, aCollapse, mCollapse, callAction }) => {
+ createComponent({
+ props: {
+ diffFile: {
+ ...diffFile,
+ viewer: {
+ ...diffFile.viewer,
+ automaticallyCollapsed: aCollapse,
+ manuallyCollapsed: mCollapse,
+ },
+ },
+ showLocalFileReviews: true,
+ addMergeRequestButtons: true,
+ },
+ });
+
+ findReviewFileCheckbox().vm.$emit('change', newReviewedStatus);
+
+ if (callAction) {
+ expect(mockStoreConfig.modules.diffs.actions.setFileCollapsedByUser).toHaveBeenCalled();
+ } else {
+ expect(
+ mockStoreConfig.modules.diffs.actions.setFileCollapsedByUser,
+ ).not.toHaveBeenCalled();
+ }
+ },
+ );
+
+ it.each`
+ description | show | visible
+ ${'shows'} | ${true} | ${true}
+ ${'hides'} | ${false} | ${false}
+ `(
+ '$description the file review feature given { showLocalFileReviewsProp: $show }',
+ ({ show, visible }) => {
+ createComponent({
+ props: {
+ showLocalFileReviews: show,
+ addMergeRequestButtons: true,
+ },
+ });
+
+ expect(findReviewFileCheckbox().exists()).toEqual(visible);
+ },
+ );
+
+ it.each`
+ open | status | fires
+ ${true} | ${true} | ${true}
+ ${false} | ${false} | ${true}
+ ${true} | ${false} | ${false}
+ ${false} | ${true} | ${false}
+ `(
+ 'toggles appropriately when { fileExpanded: $open, newReviewStatus: $status }',
+ ({ open, status, fires }) => {
+ createComponent({
+ props: {
+ diffFile: {
+ ...diffFile,
+ viewer: {
+ ...diffFile.viewer,
+ automaticallyCollapsed: false,
+ manuallyCollapsed: null,
+ },
+ },
+ showLocalFileReviews: true,
+ addMergeRequestButtons: true,
+ expanded: open,
+ },
+ });
+
+ findReviewFileCheckbox().vm.$emit('change', status);
+
+ expect(Boolean(wrapper.emitted().toggleFile)).toBe(fires);
+ },
+ );
+ });
});
diff --git a/spec/frontend/diffs/components/diff_file_row_spec.js b/spec/frontend/diffs/components/diff_file_row_spec.js
index 7403a7918a9..1d1c5fec293 100644
--- a/spec/frontend/diffs/components/diff_file_row_spec.js
+++ b/spec/frontend/diffs/components/diff_file_row_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
import DiffFileRow from '~/diffs/components/diff_file_row.vue';
-import FileRow from '~/vue_shared/components/file_row.vue';
import FileRowStats from '~/diffs/components/file_row_stats.vue';
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
+import FileRow from '~/vue_shared/components/file_row.vue';
describe('Diff File Row component', () => {
let wrapper;
diff --git a/spec/frontend/diffs/components/diff_file_spec.js b/spec/frontend/diffs/components/diff_file_spec.js
index c715d779986..9c3c3e82ad5 100644
--- a/spec/frontend/diffs/components/diff_file_spec.js
+++ b/spec/frontend/diffs/components/diff_file_spec.js
@@ -1,26 +1,25 @@
-import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import Vuex from 'vuex';
-import axios from '~/lib/utils/axios_utils';
-import httpStatus from '~/lib/utils/http_status';
-import createDiffsStore from '~/diffs/store/modules';
-import createNotesStore from '~/notes/stores/modules';
-import diffFileMockDataReadable from '../mock_data/diff_file';
-import diffFileMockDataUnreadable from '../mock_data/diff_file_unreadable';
-
+import DiffContentComponent from '~/diffs/components/diff_content.vue';
import DiffFileComponent from '~/diffs/components/diff_file.vue';
import DiffFileHeaderComponent from '~/diffs/components/diff_file_header.vue';
-import DiffContentComponent from '~/diffs/components/diff_content.vue';
-import eventHub from '~/diffs/event_hub';
import {
EVT_EXPAND_ALL_FILES,
EVT_PERF_MARK_DIFF_FILES_END,
EVT_PERF_MARK_FIRST_DIFF_FILE_SHOWN,
} from '~/diffs/constants';
+import eventHub from '~/diffs/event_hub';
+import createDiffsStore from '~/diffs/store/modules';
import { diffViewerModes, diffViewerErrors } from '~/ide/constants';
+import axios from '~/lib/utils/axios_utils';
+import httpStatus from '~/lib/utils/http_status';
+import createNotesStore from '~/notes/stores/modules';
+import diffFileMockDataReadable from '../mock_data/diff_file';
+import diffFileMockDataUnreadable from '../mock_data/diff_file_unreadable';
function changeViewer(store, index, { automaticallyCollapsed, manuallyCollapsed, name }) {
const file = store.state.diffs.diffFiles[index];
@@ -66,7 +65,7 @@ function markFileToBeRendered(store, index = 0) {
});
}
-function createComponent({ file, first = false, last = false }) {
+function createComponent({ file, first = false, last = false, options = {}, props = {} }) {
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -89,7 +88,9 @@ function createComponent({ file, first = false, last = false }) {
viewDiffsFileByFile: false,
isFirstFile: first,
isLastFile: last,
+ ...props,
},
+ ...options,
});
return {
@@ -220,6 +221,53 @@ describe('DiffFile', () => {
});
});
+ describe('computed', () => {
+ describe('showLocalFileReviews', () => {
+ let gon;
+
+ function setLoggedIn(bool) {
+ window.gon.current_user_id = bool;
+ }
+
+ beforeAll(() => {
+ gon = window.gon;
+ window.gon = {};
+ });
+
+ afterEach(() => {
+ window.gon = gon;
+ });
+
+ it.each`
+ loggedIn | featureOn | bool
+ ${true} | ${true} | ${true}
+ ${false} | ${true} | ${false}
+ ${true} | ${false} | ${false}
+ ${false} | ${false} | ${false}
+ `(
+ 'should be $bool when { userIsLoggedIn: $loggedIn, featureEnabled: $featureOn }',
+ ({ loggedIn, featureOn, bool }) => {
+ setLoggedIn(loggedIn);
+
+ ({ wrapper } = createComponent({
+ options: {
+ provide: {
+ glFeatures: {
+ localFileReviews: featureOn,
+ },
+ },
+ },
+ props: {
+ file: store.state.diffs.diffFiles[0],
+ },
+ }));
+
+ expect(wrapper.vm.showLocalFileReviews).toBe(bool);
+ },
+ );
+ });
+ });
+
describe('collapsing', () => {
describe(`\`${EVT_EXPAND_ALL_FILES}\` event`, () => {
beforeEach(() => {
@@ -422,9 +470,11 @@ describe('DiffFile', () => {
await wrapper.vm.$nextTick();
- expect(wrapper.vm.$el.innerText).toContain(
- 'This source diff could not be displayed because it is too large',
- );
+ const button = wrapper.find('[data-testid="blob-button"]');
+
+ expect(wrapper.text()).toContain('Changes are too large to be shown.');
+ expect(button.html()).toContain('View file @');
+ expect(button.attributes('href')).toBe('/file/view/path');
});
});
});
diff --git a/spec/frontend/diffs/components/diff_line_note_form_spec.js b/spec/frontend/diffs/components/diff_line_note_form_spec.js
index faa68159c58..a192f7e2e9a 100644
--- a/spec/frontend/diffs/components/diff_line_note_form_spec.js
+++ b/spec/frontend/diffs/components/diff_line_note_form_spec.js
@@ -1,9 +1,9 @@
import { shallowMount } from '@vue/test-utils';
import DiffLineNoteForm from '~/diffs/components/diff_line_note_form.vue';
-import NoteForm from '~/notes/components/note_form.vue';
import { createStore } from '~/mr_notes/stores';
-import diffFileMockData from '../mock_data/diff_file';
+import NoteForm from '~/notes/components/note_form.vue';
import { noteableDataMock } from '../../notes/mock_data';
+import diffFileMockData from '../mock_data/diff_file';
describe('DiffLineNoteForm', () => {
let wrapper;
@@ -17,6 +17,7 @@ describe('DiffLineNoteForm', () => {
const store = createStore();
store.state.notes.userData.id = 1;
store.state.notes.noteableData = noteableDataMock;
+ store.state.diffs.diffFiles = [diffFile];
store.replaceState({ ...store.state, ...args.state });
diff --git a/spec/frontend/diffs/components/diff_row_spec.js b/spec/frontend/diffs/components/diff_row_spec.js
index c06d8e78316..5682b29d697 100644
--- a/spec/frontend/diffs/components/diff_row_spec.js
+++ b/spec/frontend/diffs/components/diff_row_spec.js
@@ -1,10 +1,10 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
import { getByTestId, fireEvent } from '@testing-library/dom';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import diffsModule from '~/diffs/store/modules';
import DiffRow from '~/diffs/components/diff_row.vue';
-import diffFileMockData from '../mock_data/diff_file';
import { mapParallel } from '~/diffs/components/diff_row_utils';
+import diffsModule from '~/diffs/store/modules';
+import diffFileMockData from '../mock_data/diff_file';
describe('DiffRow', () => {
const testLines = [
diff --git a/spec/frontend/diffs/components/diff_row_utils_spec.js b/spec/frontend/diffs/components/diff_row_utils_spec.js
index d70d6b609ac..47ae3cd5867 100644
--- a/spec/frontend/diffs/components/diff_row_utils_spec.js
+++ b/spec/frontend/diffs/components/diff_row_utils_spec.js
@@ -143,10 +143,21 @@ describe('addCommentTooltip', () => {
'Commenting on symbolic links that replace or are replaced by files is currently not supported.';
const brokenRealTooltip =
'Commenting on files that replace or are replaced by symbolic links is currently not supported.';
+ const commentTooltip = 'Add a comment to this line';
+ const dragTooltip = 'Add a comment to this line or drag for multiple lines';
+
it('should return default tooltip', () => {
expect(utils.addCommentTooltip()).toBeUndefined();
});
+ it('should return comment tooltip', () => {
+ expect(utils.addCommentTooltip({})).toEqual(commentTooltip);
+ });
+
+ it('should return drag comment tooltip when dragging is enabled', () => {
+ expect(utils.addCommentTooltip({}, true)).toEqual(dragTooltip);
+ });
+
it('should return broken symlink tooltip', () => {
expect(utils.addCommentTooltip({ commentsDisabled: { wasSymbolic: true } })).toEqual(
brokenSymLinkTooltip,
diff --git a/spec/frontend/diffs/components/diff_stats_spec.js b/spec/frontend/diffs/components/diff_stats_spec.js
index 0aaec027c0a..504158fb7fc 100644
--- a/spec/frontend/diffs/components/diff_stats_spec.js
+++ b/spec/frontend/diffs/components/diff_stats_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import DiffStats from '~/diffs/components/diff_stats.vue';
const TEST_ADDED_LINES = 100;
diff --git a/spec/frontend/diffs/components/diff_view_spec.js b/spec/frontend/diffs/components/diff_view_spec.js
index 3d36ebf14a3..83b173c1f5d 100644
--- a/spec/frontend/diffs/components/diff_view_spec.js
+++ b/spec/frontend/diffs/components/diff_view_spec.js
@@ -1,5 +1,5 @@
-import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import DiffView from '~/diffs/components/diff_view.vue';
@@ -55,12 +55,12 @@ describe('DiffView', () => {
});
it.each`
- type | side | container | sides | total
- ${'parallel'} | ${'left'} | ${'.old'} | ${{ left: { lineDraft: {} }, right: { lineDraft: {} } }} | ${2}
- ${'parallel'} | ${'right'} | ${'.new'} | ${{ left: { lineDraft: {} }, right: { lineDraft: {} } }} | ${2}
- ${'inline'} | ${'left'} | ${'.old'} | ${{ left: { lineDraft: {} } }} | ${1}
- ${'inline'} | ${'right'} | ${'.new'} | ${{ right: { lineDraft: {} } }} | ${1}
- ${'inline'} | ${'left'} | ${'.old'} | ${{ left: { lineDraft: {} }, right: { lineDraft: {} } }} | ${1}
+ type | side | container | sides | total
+ ${'parallel'} | ${'left'} | ${'.old'} | ${{ left: { lineDraft: {}, renderDiscussion: true }, right: { lineDraft: {}, renderDiscussion: true } }} | ${2}
+ ${'parallel'} | ${'right'} | ${'.new'} | ${{ left: { lineDraft: {}, renderDiscussion: true }, right: { lineDraft: {}, renderDiscussion: true } }} | ${2}
+ ${'inline'} | ${'left'} | ${'.old'} | ${{ left: { lineDraft: {}, renderDiscussion: true } }} | ${1}
+ ${'inline'} | ${'left'} | ${'.old'} | ${{ left: { lineDraft: {}, renderDiscussion: true } }} | ${1}
+ ${'inline'} | ${'left'} | ${'.old'} | ${{ left: { lineDraft: {}, renderDiscussion: true } }} | ${1}
`(
'renders a $type comment row with comment cell on $side',
({ type, container, sides, total }) => {
diff --git a/spec/frontend/diffs/components/image_diff_overlay_spec.js b/spec/frontend/diffs/components/image_diff_overlay_spec.js
index 93c9b922fdd..47b144b2387 100644
--- a/spec/frontend/diffs/components/image_diff_overlay_spec.js
+++ b/spec/frontend/diffs/components/image_diff_overlay_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue';
import { createStore } from '~/mr_notes/stores';
import { imageDiffDiscussions } from '../mock_data/diff_discussions';
diff --git a/spec/frontend/diffs/components/inline_diff_table_row_spec.js b/spec/frontend/diffs/components/inline_diff_table_row_spec.js
index 21e7d7397a0..7e6f75ad6f8 100644
--- a/spec/frontend/diffs/components/inline_diff_table_row_spec.js
+++ b/spec/frontend/diffs/components/inline_diff_table_row_spec.js
@@ -1,10 +1,10 @@
import { shallowMount } from '@vue/test-utils';
-import { createStore } from '~/mr_notes/stores';
-import InlineDiffTableRow from '~/diffs/components/inline_diff_table_row.vue';
import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue';
-import diffFileMockData from '../mock_data/diff_file';
-import discussionsMockData from '../mock_data/diff_discussions';
import { mapInline } from '~/diffs/components/diff_row_utils';
+import InlineDiffTableRow from '~/diffs/components/inline_diff_table_row.vue';
+import { createStore } from '~/mr_notes/stores';
+import discussionsMockData from '../mock_data/diff_discussions';
+import diffFileMockData from '../mock_data/diff_file';
const TEST_USER_ID = 'abc123';
const TEST_USER = { id: TEST_USER_ID };
diff --git a/spec/frontend/diffs/components/inline_diff_view_spec.js b/spec/frontend/diffs/components/inline_diff_view_spec.js
index 6a1791509fd..27834804f77 100644
--- a/spec/frontend/diffs/components/inline_diff_view_spec.js
+++ b/spec/frontend/diffs/components/inline_diff_view_spec.js
@@ -1,11 +1,11 @@
import '~/behaviors/markdown/render_gfm';
-import { mount } from '@vue/test-utils';
import { getByText } from '@testing-library/dom';
-import { createStore } from '~/mr_notes/stores';
-import InlineDiffView from '~/diffs/components/inline_diff_view.vue';
+import { mount } from '@vue/test-utils';
import { mapInline } from '~/diffs/components/diff_row_utils';
-import diffFileMockData from '../mock_data/diff_file';
+import InlineDiffView from '~/diffs/components/inline_diff_view.vue';
+import { createStore } from '~/mr_notes/stores';
import discussionsMockData from '../mock_data/diff_discussions';
+import diffFileMockData from '../mock_data/diff_file';
describe('InlineDiffView', () => {
let wrapper;
diff --git a/spec/frontend/diffs/components/no_changes_spec.js b/spec/frontend/diffs/components/no_changes_spec.js
index df9af51f9cf..164c58dc8e4 100644
--- a/spec/frontend/diffs/components/no_changes_spec.js
+++ b/spec/frontend/diffs/components/no_changes_spec.js
@@ -1,8 +1,8 @@
+import { GlButton } from '@gitlab/ui';
import { createLocalVue, shallowMount, mount } from '@vue/test-utils';
import Vuex from 'vuex';
-import { GlButton } from '@gitlab/ui';
-import { createStore } from '~/mr_notes/stores';
import NoChanges from '~/diffs/components/no_changes.vue';
+import { createStore } from '~/mr_notes/stores';
import diffsMockData from '../mock_data/merge_request_diffs';
const localVue = createLocalVue();
diff --git a/spec/frontend/diffs/components/parallel_diff_table_row_spec.js b/spec/frontend/diffs/components/parallel_diff_table_row_spec.js
index 445553706b7..dbe8303077d 100644
--- a/spec/frontend/diffs/components/parallel_diff_table_row_spec.js
+++ b/spec/frontend/diffs/components/parallel_diff_table_row_spec.js
@@ -1,12 +1,12 @@
-import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
-import { createStore } from '~/mr_notes/stores';
-import ParallelDiffTableRow from '~/diffs/components/parallel_diff_table_row.vue';
-import { mapParallel } from '~/diffs/components/diff_row_utils';
-import diffFileMockData from '../mock_data/diff_file';
import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue';
+import { mapParallel } from '~/diffs/components/diff_row_utils';
+import ParallelDiffTableRow from '~/diffs/components/parallel_diff_table_row.vue';
+import { createStore } from '~/mr_notes/stores';
import discussionsMockData from '../mock_data/diff_discussions';
+import diffFileMockData from '../mock_data/diff_file';
describe('ParallelDiffTableRow', () => {
const mockDiffContent = {
diff --git a/spec/frontend/diffs/components/parallel_diff_view_spec.js b/spec/frontend/diffs/components/parallel_diff_view_spec.js
index 44ed303d0ef..452e1f58551 100644
--- a/spec/frontend/diffs/components/parallel_diff_view_spec.js
+++ b/spec/frontend/diffs/components/parallel_diff_view_spec.js
@@ -1,8 +1,8 @@
-import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
-import { createStore } from '~/mr_notes/stores';
-import ParallelDiffView from '~/diffs/components/parallel_diff_view.vue';
+import Vuex from 'vuex';
import parallelDiffTableRow from '~/diffs/components/parallel_diff_table_row.vue';
+import ParallelDiffView from '~/diffs/components/parallel_diff_view.vue';
+import { createStore } from '~/mr_notes/stores';
import diffFileMockData from '../mock_data/diff_file';
let wrapper;
diff --git a/spec/frontend/diffs/components/settings_dropdown_spec.js b/spec/frontend/diffs/components/settings_dropdown_spec.js
index fcb627c570a..99fa83b64f1 100644
--- a/spec/frontend/diffs/components/settings_dropdown_spec.js
+++ b/spec/frontend/diffs/components/settings_dropdown_spec.js
@@ -1,6 +1,5 @@
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import diffModule from '~/diffs/store/modules';
import SettingsDropdown from '~/diffs/components/settings_dropdown.vue';
import {
EVT_VIEW_FILE_BY_FILE,
@@ -8,6 +7,7 @@ import {
INLINE_DIFF_VIEW_TYPE,
} from '~/diffs/constants';
import eventHub from '~/diffs/event_hub';
+import diffModule from '~/diffs/store/modules';
const localVue = createLocalVue();
localVue.use(Vuex);
diff --git a/spec/frontend/diffs/components/tree_list_spec.js b/spec/frontend/diffs/components/tree_list_spec.js
index 4666321e0c2..f316a9fdf01 100644
--- a/spec/frontend/diffs/components/tree_list_spec.js
+++ b/spec/frontend/diffs/components/tree_list_spec.js
@@ -1,5 +1,5 @@
-import Vuex from 'vuex';
import { shallowMount, mount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
import TreeList from '~/diffs/components/tree_list.vue';
import createStore from '~/diffs/store/modules';
import FileTree from '~/vue_shared/components/file_tree.vue';
diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js
index 056ac23fcf7..ed3210ecfaf 100644
--- a/spec/frontend/diffs/store/actions_spec.js
+++ b/spec/frontend/diffs/store/actions_spec.js
@@ -1,9 +1,9 @@
import MockAdapter from 'axios-mock-adapter';
import Cookies from 'js-cookie';
-import mockDiffFile from 'jest/diffs/mock_data/diff_file';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
+import mockDiffFile from 'jest/diffs/mock_data/diff_file';
import {
DIFF_VIEW_COOKIE_NAME,
INLINE_DIFF_VIEW_TYPE,
@@ -52,14 +52,14 @@ import {
setFileByFile,
reviewFile,
} from '~/diffs/store/actions';
-import eventHub from '~/notes/event_hub';
import * as types from '~/diffs/store/mutation_types';
-import axios from '~/lib/utils/axios_utils';
import * as utils from '~/diffs/store/utils';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
import * as commonUtils from '~/lib/utils/common_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
+import eventHub from '~/notes/event_hub';
import { diffMetadata } from '../mock_data/diff_metadata';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
jest.mock('~/flash');
diff --git a/spec/frontend/diffs/store/getters_spec.js b/spec/frontend/diffs/store/getters_spec.js
index 4d7f861ac22..04606b48662 100644
--- a/spec/frontend/diffs/store/getters_spec.js
+++ b/spec/frontend/diffs/store/getters_spec.js
@@ -1,6 +1,6 @@
+import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '~/diffs/constants';
import * as getters from '~/diffs/store/getters';
import state from '~/diffs/store/modules/diff_state';
-import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '~/diffs/constants';
import discussion from '../mock_data/diff_discussions';
describe('Diffs Module Getters', () => {
@@ -376,24 +376,62 @@ describe('Diffs Module Getters', () => {
});
});
- describe('fileReviews', () => {
- const file1 = { id: '123', file_identifier_hash: 'abc' };
- const file2 = { id: '098', file_identifier_hash: 'abc' };
+ describe('suggestionCommitMessage', () => {
+ beforeEach(() => {
+ Object.assign(localState, {
+ defaultSuggestionCommitMessage:
+ '%{branch_name}%{project_path}%{project_name}%{username}%{user_full_name}%{file_paths}%{suggestions_count}%{files_count}',
+ branchName: 'branch',
+ projectPath: '/path',
+ projectName: 'name',
+ username: 'user',
+ userFullName: 'user userton',
+ });
+ });
it.each`
- reviews | files | fileReviews
- ${{}} | ${[file1, file2]} | ${[false, false]}
- ${{ abc: ['123'] }} | ${[file1, file2]} | ${[true, false]}
- ${{ abc: ['098'] }} | ${[file1, file2]} | ${[false, true]}
- ${{ def: ['123'] }} | ${[file1, file2]} | ${[false, false]}
- ${{ abc: ['123'], def: ['098'] }} | ${[]} | ${[]}
+ specialState | output
+ ${{}} | ${'branch/pathnameuseruser userton%{file_paths}%{suggestions_count}%{files_count}'}
+ ${{ userFullName: null }} | ${'branch/pathnameuser%{user_full_name}%{file_paths}%{suggestions_count}%{files_count}'}
+ ${{ username: null }} | ${'branch/pathname%{username}user userton%{file_paths}%{suggestions_count}%{files_count}'}
+ ${{ projectName: null }} | ${'branch/path%{project_name}useruser userton%{file_paths}%{suggestions_count}%{files_count}'}
+ ${{ projectPath: null }} | ${'branch%{project_path}nameuseruser userton%{file_paths}%{suggestions_count}%{files_count}'}
+ ${{ branchName: null }} | ${'%{branch_name}/pathnameuseruser userton%{file_paths}%{suggestions_count}%{files_count}'}
`(
- 'returns $fileReviews based on the diff files in state and the existing reviews $reviews',
- ({ reviews, files, fileReviews }) => {
- localState.diffFiles = files;
- localState.mrReviews = reviews;
+ 'provides the correct "base" default commit message based on state ($specialState)',
+ ({ specialState, output }) => {
+ Object.assign(localState, specialState);
+
+ expect(getters.suggestionCommitMessage(localState)()).toBe(output);
+ },
+ );
- expect(getters.fileReviews(localState)).toStrictEqual(fileReviews);
+ it.each`
+ stateOverrides | output
+ ${{}} | ${'branch/pathnameuseruser userton%{file_paths}%{suggestions_count}%{files_count}'}
+ ${{ user_full_name: null }} | ${'branch/pathnameuser%{user_full_name}%{file_paths}%{suggestions_count}%{files_count}'}
+ ${{ username: null }} | ${'branch/pathname%{username}user userton%{file_paths}%{suggestions_count}%{files_count}'}
+ ${{ project_name: null }} | ${'branch/path%{project_name}useruser userton%{file_paths}%{suggestions_count}%{files_count}'}
+ ${{ project_path: null }} | ${'branch%{project_path}nameuseruser userton%{file_paths}%{suggestions_count}%{files_count}'}
+ ${{ branch_name: null }} | ${'%{branch_name}/pathnameuseruser userton%{file_paths}%{suggestions_count}%{files_count}'}
+ `(
+ "properly overrides state values ($stateOverrides) if they're provided",
+ ({ stateOverrides, output }) => {
+ expect(getters.suggestionCommitMessage(localState)(stateOverrides)).toBe(output);
+ },
+ );
+
+ it.each`
+ providedValues | output
+ ${{ file_paths: 'path1, path2', suggestions_count: 1, files_count: 1 }} | ${'branch/pathnameuseruser usertonpath1, path211'}
+ ${{ suggestions_count: 1, files_count: 1 }} | ${'branch/pathnameuseruser userton%{file_paths}11'}
+ ${{ file_paths: 'path1, path2', files_count: 1 }} | ${'branch/pathnameuseruser usertonpath1, path2%{suggestions_count}1'}
+ ${{ file_paths: 'path1, path2', suggestions_count: 1 }} | ${'branch/pathnameuseruser usertonpath1, path21%{files_count}'}
+ ${{ something_unused: 'CrAzY TeXt' }} | ${'branch/pathnameuseruser userton%{file_paths}%{suggestions_count}%{files_count}'}
+ `(
+ "fills in any missing interpolations ($providedValues) when they're provided at the getter callsite",
+ ({ providedValues, output }) => {
+ expect(getters.suggestionCommitMessage(localState)(providedValues)).toBe(output);
},
);
});
diff --git a/spec/frontend/diffs/store/getters_versions_dropdowns_spec.js b/spec/frontend/diffs/store/getters_versions_dropdowns_spec.js
index f7954515422..dbef547c297 100644
--- a/spec/frontend/diffs/store/getters_versions_dropdowns_spec.js
+++ b/spec/frontend/diffs/store/getters_versions_dropdowns_spec.js
@@ -1,9 +1,9 @@
-import * as getters from '~/diffs/store/getters';
-import state from '~/diffs/store/modules/diff_state';
import {
DIFF_COMPARE_BASE_VERSION_INDEX,
DIFF_COMPARE_HEAD_VERSION_INDEX,
} from '~/diffs/constants';
+import * as getters from '~/diffs/store/getters';
+import state from '~/diffs/store/modules/diff_state';
import diffsMockData from '../mock_data/merge_request_diffs';
describe('Compare diff version dropdowns', () => {
diff --git a/spec/frontend/diffs/store/mutations_spec.js b/spec/frontend/diffs/store/mutations_spec.js
index 2c342d8e2a5..a8ae759e693 100644
--- a/spec/frontend/diffs/store/mutations_spec.js
+++ b/spec/frontend/diffs/store/mutations_spec.js
@@ -1,9 +1,9 @@
+import { INLINE_DIFF_VIEW_TYPE, INLINE_DIFF_LINES_KEY } from '~/diffs/constants';
import createState from '~/diffs/store/modules/diff_state';
-import mutations from '~/diffs/store/mutations';
import * as types from '~/diffs/store/mutation_types';
-import { INLINE_DIFF_VIEW_TYPE, INLINE_DIFF_LINES_KEY } from '~/diffs/constants';
-import diffFileMockData from '../mock_data/diff_file';
+import mutations from '~/diffs/store/mutations';
import * as utils from '~/diffs/store/utils';
+import diffFileMockData from '../mock_data/diff_file';
describe('DiffsStoreMutations', () => {
describe('SET_BASE_CONFIG', () => {
diff --git a/spec/frontend/diffs/store/utils_spec.js b/spec/frontend/diffs/store/utils_spec.js
index a19e5e91677..dcb58f7a380 100644
--- a/spec/frontend/diffs/store/utils_spec.js
+++ b/spec/frontend/diffs/store/utils_spec.js
@@ -1,5 +1,4 @@
import { clone } from 'lodash';
-import * as utils from '~/diffs/store/utils';
import {
LINE_POSITION_LEFT,
LINE_POSITION_RIGHT,
@@ -12,10 +11,11 @@ import {
INLINE_DIFF_VIEW_TYPE,
INLINE_DIFF_LINES_KEY,
} from '~/diffs/constants';
+import * as utils from '~/diffs/store/utils';
import { MERGE_REQUEST_NOTEABLE_TYPE } from '~/notes/constants';
+import { noteableDataMock } from '../../notes/mock_data';
import diffFileMockData from '../mock_data/diff_file';
import { diffMetadata } from '../mock_data/diff_metadata';
-import { noteableDataMock } from '../../notes/mock_data';
const getDiffFileMock = () => JSON.parse(JSON.stringify(diffFileMockData));
const getDiffMetadataMock = () => JSON.parse(JSON.stringify(diffMetadata));
diff --git a/spec/frontend/diffs/utils/diff_file_spec.js b/spec/frontend/diffs/utils/diff_file_spec.js
index 2de8db28e71..c6cfdfced65 100644
--- a/spec/frontend/diffs/utils/diff_file_spec.js
+++ b/spec/frontend/diffs/utils/diff_file_spec.js
@@ -1,4 +1,4 @@
-import { prepareRawDiffFile } from '~/diffs/utils/diff_file';
+import { prepareRawDiffFile, getShortShaFromFile } from '~/diffs/utils/diff_file';
function getDiffFiles() {
const loadFull = 'namespace/project/-/merge_requests/12345/diff_for_path?file_identifier=abc';
@@ -143,4 +143,15 @@ describe('diff_file utilities', () => {
expect(preppedFile).not.toHaveProp('id');
});
});
+
+ describe('getShortShaFromFile', () => {
+ it.each`
+ response | cs
+ ${'12345678'} | ${'12345678abcdogcat'}
+ ${null} | ${undefined}
+ ${'hidogcat'} | ${'hidogcatmorethings'}
+ `('returns $response for a file with { content_sha: $cs }', ({ response, cs }) => {
+ expect(getShortShaFromFile({ content_sha: cs })).toBe(response);
+ });
+ });
});
diff --git a/spec/frontend/diffs/utils/file_reviews_spec.js b/spec/frontend/diffs/utils/file_reviews_spec.js
index 819426ee75f..a58c19a7245 100644
--- a/spec/frontend/diffs/utils/file_reviews_spec.js
+++ b/spec/frontend/diffs/utils/file_reviews_spec.js
@@ -5,6 +5,7 @@ import {
setReviewsForMergeRequest,
isFileReviewed,
markFileReview,
+ reviewStatuses,
reviewable,
} from '~/diffs/utils/file_reviews';
@@ -28,6 +29,39 @@ describe('File Review(s) utilities', () => {
localStorage.clear();
});
+ describe('isFileReviewed', () => {
+ it.each`
+ description | diffFile | fileReviews
+ ${'the file does not have an `id`'} | ${{ ...file, id: undefined }} | ${getDefaultReviews()}
+ ${'there are no reviews for the file'} | ${file} | ${{ ...getDefaultReviews(), abc: undefined }}
+ `('returns `false` if $description', ({ diffFile, fileReviews }) => {
+ expect(isFileReviewed(fileReviews, diffFile)).toBe(false);
+ });
+
+ it("returns `true` for a file if it's available in the provided reviews", () => {
+ expect(isFileReviewed(reviews, file)).toBe(true);
+ });
+ });
+
+ describe('reviewStatuses', () => {
+ const file1 = { id: '123', file_identifier_hash: 'abc' };
+ const file2 = { id: '098', file_identifier_hash: 'abc' };
+
+ it.each`
+ mrReviews | files | fileReviews
+ ${{}} | ${[file1, file2]} | ${[false, false]}
+ ${{ abc: ['123'] }} | ${[file1, file2]} | ${[true, false]}
+ ${{ abc: ['098'] }} | ${[file1, file2]} | ${[false, true]}
+ ${{ def: ['123'] }} | ${[file1, file2]} | ${[false, false]}
+ ${{ abc: ['123'], def: ['098'] }} | ${[]} | ${[]}
+ `(
+ 'returns $fileReviews based on the diff files in state and the existing reviews $reviews',
+ ({ mrReviews, files, fileReviews }) => {
+ expect(reviewStatuses(files, mrReviews)).toStrictEqual(fileReviews);
+ },
+ );
+ });
+
describe('getReviewsForMergeRequest', () => {
it('fetches the appropriate stored reviews from localStorage', () => {
getReviewsForMergeRequest(mrPath);
@@ -73,20 +107,6 @@ describe('File Review(s) utilities', () => {
});
});
- describe('isFileReviewed', () => {
- it.each`
- description | diffFile | fileReviews
- ${'the file does not have an `id`'} | ${{ ...file, id: undefined }} | ${getDefaultReviews()}
- ${'there are no reviews for the file'} | ${file} | ${{ ...getDefaultReviews(), abc: undefined }}
- `('returns `false` if $description', ({ diffFile, fileReviews }) => {
- expect(isFileReviewed(fileReviews, diffFile)).toBe(false);
- });
-
- it("returns `true` for a file if it's available in the provided reviews", () => {
- expect(isFileReviewed(reviews, file)).toBe(true);
- });
- });
-
describe('reviewable', () => {
it.each`
response | diffFile | description
diff --git a/spec/frontend/diffs/utils/preferences_spec.js b/spec/frontend/diffs/utils/preferences_spec.js
index a48db1d7512..b09db2c1003 100644
--- a/spec/frontend/diffs/utils/preferences_spec.js
+++ b/spec/frontend/diffs/utils/preferences_spec.js
@@ -1,12 +1,11 @@
import Cookies from 'js-cookie';
-import { getParameterValues } from '~/lib/utils/url_utility';
-
-import { fileByFile } from '~/diffs/utils/preferences';
import {
DIFF_FILE_BY_FILE_COOKIE_NAME,
DIFF_VIEW_FILE_BY_FILE,
DIFF_VIEW_ALL_FILES,
} from '~/diffs/constants';
+import { fileByFile } from '~/diffs/utils/preferences';
+import { getParameterValues } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility');
diff --git a/spec/frontend/diffs/utils/suggestions_spec.js b/spec/frontend/diffs/utils/suggestions_spec.js
new file mode 100644
index 00000000000..fbfe9cef857
--- /dev/null
+++ b/spec/frontend/diffs/utils/suggestions_spec.js
@@ -0,0 +1,15 @@
+import { computeSuggestionCommitMessage } from '~/diffs/utils/suggestions';
+
+describe('Diff Suggestions utilities', () => {
+ describe('computeSuggestionCommitMessage', () => {
+ it.each`
+ description | input | values | output
+ ${'makes the appropriate replacements'} | ${'%{foo} %{bar}'} | ${{ foo: 'foo', bar: 'bar' }} | ${'foo bar'}
+ ${"skips replacing values that aren't passed"} | ${'%{foo} %{bar}'} | ${{ foo: 'foo' }} | ${'foo %{bar}'}
+ ${'treats the number 0 as a valid value (not falsey)'} | ${'%{foo} %{bar}'} | ${{ foo: 'foo', bar: 0 }} | ${'foo 0'}
+ ${"works when the variables don't have any space between them"} | ${'%{foo}%{bar}'} | ${{ foo: 'foo', bar: 'bar' }} | ${'foobar'}
+ `('$description', ({ input, output, values }) => {
+ expect(computeSuggestionCommitMessage({ message: input, values })).toBe(output);
+ });
+ });
+});
diff --git a/spec/frontend/dirty_submit/dirty_submit_factory_spec.js b/spec/frontend/dirty_submit/dirty_submit_factory_spec.js
index 40843a68582..5822ccf861b 100644
--- a/spec/frontend/dirty_submit/dirty_submit_factory_spec.js
+++ b/spec/frontend/dirty_submit/dirty_submit_factory_spec.js
@@ -1,6 +1,6 @@
+import DirtySubmitCollection from '~/dirty_submit/dirty_submit_collection';
import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory';
import DirtySubmitForm from '~/dirty_submit/dirty_submit_form';
-import DirtySubmitCollection from '~/dirty_submit/dirty_submit_collection';
import { createForm } from './helper';
describe('DirtySubmitCollection', () => {
diff --git a/spec/frontend/droplab/drop_down_spec.js b/spec/frontend/droplab/drop_down_spec.js
index c1fbda6f211..dcdbbcd4ccf 100644
--- a/spec/frontend/droplab/drop_down_spec.js
+++ b/spec/frontend/droplab/drop_down_spec.js
@@ -1,6 +1,6 @@
+import { SELECTED_CLASS } from '~/droplab/constants';
import DropDown from '~/droplab/drop_down';
import utils from '~/droplab/utils';
-import { SELECTED_CLASS } from '~/droplab/constants';
describe('DropLab DropDown', () => {
let testContext;
diff --git a/spec/frontend/droplab/hook_spec.js b/spec/frontend/droplab/hook_spec.js
index 11488cab521..0b897a570f6 100644
--- a/spec/frontend/droplab/hook_spec.js
+++ b/spec/frontend/droplab/hook_spec.js
@@ -1,5 +1,5 @@
-import Hook from '~/droplab/hook';
import DropDown from '~/droplab/drop_down';
+import Hook from '~/droplab/hook';
jest.mock('~/droplab/drop_down', () => jest.fn());
diff --git a/spec/frontend/droplab/plugins/ajax_filter_spec.js b/spec/frontend/droplab/plugins/ajax_filter_spec.js
index d91884a60e5..d442d5cf416 100644
--- a/spec/frontend/droplab/plugins/ajax_filter_spec.js
+++ b/spec/frontend/droplab/plugins/ajax_filter_spec.js
@@ -1,5 +1,5 @@
-import AjaxCache from '~/lib/utils/ajax_cache';
import AjaxFilter from '~/droplab/plugins/ajax_filter';
+import AjaxCache from '~/lib/utils/ajax_cache';
describe('AjaxFilter', () => {
let dummyConfig;
diff --git a/spec/frontend/droplab/plugins/ajax_spec.js b/spec/frontend/droplab/plugins/ajax_spec.js
index 1d7576ce420..7c6452e8337 100644
--- a/spec/frontend/droplab/plugins/ajax_spec.js
+++ b/spec/frontend/droplab/plugins/ajax_spec.js
@@ -1,5 +1,5 @@
-import AjaxCache from '~/lib/utils/ajax_cache';
import Ajax from '~/droplab/plugins/ajax';
+import AjaxCache from '~/lib/utils/ajax_cache';
describe('Ajax', () => {
describe('preprocessing', () => {
diff --git a/spec/frontend/dropzone_input_spec.js b/spec/frontend/dropzone_input_spec.js
index 4cfc6478bd2..5e6ccbd7cda 100644
--- a/spec/frontend/dropzone_input_spec.js
+++ b/spec/frontend/dropzone_input_spec.js
@@ -1,9 +1,9 @@
import $ from 'jquery';
import mock from 'xhr-mock';
-import { TEST_HOST } from 'spec/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
-import dropzoneInput from '~/dropzone_input';
+import { TEST_HOST } from 'spec/test_constants';
import PasteMarkdownTable from '~/behaviors/markdown/paste_markdown_table';
+import dropzoneInput from '~/dropzone_input';
const TEST_FILE = new File([], 'somefile.jpg');
TEST_FILE.upload = {};
diff --git a/spec/frontend/editor/editor_ci_schema_ext_spec.js b/spec/frontend/editor/editor_ci_schema_ext_spec.js
index 9dd88aad7e6..17a9ae7335f 100644
--- a/spec/frontend/editor/editor_ci_schema_ext_spec.js
+++ b/spec/frontend/editor/editor_ci_schema_ext_spec.js
@@ -1,8 +1,8 @@
import { languages } from 'monaco-editor';
import { TEST_HOST } from 'helpers/test_constants';
+import { EXTENSION_CI_SCHEMA_FILE_NAME_MATCH } from '~/editor/constants';
import EditorLite from '~/editor/editor_lite';
import { CiSchemaExtension } from '~/editor/extensions/editor_ci_schema_ext';
-import { EXTENSION_CI_SCHEMA_FILE_NAME_MATCH } from '~/editor/constants';
describe('~/editor/editor_ci_config_ext', () => {
const defaultBlobPath = '.gitlab-ci.yml';
diff --git a/spec/frontend/editor/editor_lite_spec.js b/spec/frontend/editor/editor_lite_spec.js
index c3099997287..815457e012f 100644
--- a/spec/frontend/editor/editor_lite_spec.js
+++ b/spec/frontend/editor/editor_lite_spec.js
@@ -1,14 +1,21 @@
/* eslint-disable max-classes-per-file */
-import { editor as monacoEditor, languages as monacoLanguages, Uri } from 'monaco-editor';
+import { editor as monacoEditor, languages as monacoLanguages } from 'monaco-editor';
import waitForPromises from 'helpers/wait_for_promises';
-import Editor from '~/editor/editor_lite';
+import {
+ EDITOR_LITE_INSTANCE_ERROR_NO_EL,
+ URI_PREFIX,
+ EDITOR_READY_EVENT,
+} from '~/editor/constants';
+import EditorLite from '~/editor/editor_lite';
import { EditorLiteExtension } from '~/editor/extensions/editor_lite_extension_base';
import { DEFAULT_THEME, themes } from '~/ide/lib/themes';
-import { EDITOR_LITE_INSTANCE_ERROR_NO_EL, URI_PREFIX } from '~/editor/constants';
+import { joinPaths } from '~/lib/utils/url_utility';
describe('Base editor', () => {
let editorEl;
let editor;
+ let defaultArguments;
+ const blobOriginalContent = 'Foo Foo';
const blobContent = 'Foo Bar';
const blobPath = 'test.md';
const blobGlobalId = 'snippet_777';
@@ -17,15 +24,19 @@ describe('Base editor', () => {
beforeEach(() => {
setFixtures('<div id="editor" data-editor-loading></div>');
editorEl = document.getElementById('editor');
- editor = new Editor();
+ defaultArguments = { el: editorEl, blobPath, blobContent, blobGlobalId };
+ editor = new EditorLite();
});
afterEach(() => {
editor.dispose();
editorEl.remove();
+ monacoEditor.getModels().forEach((model) => {
+ model.dispose();
+ });
});
- const createUri = (...paths) => Uri.file([URI_PREFIX, ...paths].join('/'));
+ const uriFilePath = joinPaths('/', URI_PREFIX, blobGlobalId, blobPath);
it('initializes Editor with basic properties', () => {
expect(editor).toBeDefined();
@@ -38,76 +49,192 @@ describe('Base editor', () => {
expect(editorEl.dataset.editorLoading).toBeUndefined();
});
- describe('instance of the Editor', () => {
+ describe('instance of the Editor Lite', () => {
let modelSpy;
let instanceSpy;
- let setModel;
- let dispose;
+ const setModel = jest.fn();
+ const dispose = jest.fn();
+ const mockModelReturn = (res = fakeModel) => {
+ modelSpy = jest.spyOn(monacoEditor, 'createModel').mockImplementation(() => res);
+ };
+ const mockDecorateInstance = (decorations = {}) => {
+ jest.spyOn(EditorLite, 'convertMonacoToELInstance').mockImplementation((inst) => {
+ return Object.assign(inst, decorations);
+ });
+ };
beforeEach(() => {
- setModel = jest.fn();
- dispose = jest.fn();
- modelSpy = jest.spyOn(monacoEditor, 'createModel').mockImplementation(() => fakeModel);
- instanceSpy = jest.spyOn(monacoEditor, 'create').mockImplementation(() => ({
- setModel,
- dispose,
- onDidDispose: jest.fn(),
- }));
+ modelSpy = jest.spyOn(monacoEditor, 'createModel');
});
- it('throws an error if no dom element is supplied', () => {
- expect(() => {
- editor.createInstance();
- }).toThrow(EDITOR_LITE_INSTANCE_ERROR_NO_EL);
+ describe('instance of the Code Editor', () => {
+ beforeEach(() => {
+ instanceSpy = jest.spyOn(monacoEditor, 'create');
+ });
- expect(modelSpy).not.toHaveBeenCalled();
- expect(instanceSpy).not.toHaveBeenCalled();
- expect(setModel).not.toHaveBeenCalled();
- });
+ it('throws an error if no dom element is supplied', () => {
+ mockDecorateInstance();
+ expect(() => {
+ editor.createInstance();
+ }).toThrow(EDITOR_LITE_INSTANCE_ERROR_NO_EL);
- it('creates model to be supplied to Monaco editor', () => {
- editor.createInstance({ el: editorEl, blobPath, blobContent, blobGlobalId: '' });
+ expect(modelSpy).not.toHaveBeenCalled();
+ expect(instanceSpy).not.toHaveBeenCalled();
+ expect(EditorLite.convertMonacoToELInstance).not.toHaveBeenCalled();
+ });
- expect(modelSpy).toHaveBeenCalledWith(blobContent, undefined, createUri(blobPath));
- expect(setModel).toHaveBeenCalledWith(fakeModel);
- });
+ it('creates model to be supplied to Monaco editor', () => {
+ mockModelReturn();
+ mockDecorateInstance({
+ setModel,
+ });
+ editor.createInstance(defaultArguments);
- it('initializes the instance on a supplied DOM node', () => {
- editor.createInstance({ el: editorEl });
+ expect(modelSpy).toHaveBeenCalledWith(
+ blobContent,
+ undefined,
+ expect.objectContaining({
+ path: uriFilePath,
+ }),
+ );
+ expect(setModel).toHaveBeenCalledWith(fakeModel);
+ });
- expect(editor.editorEl).not.toBe(null);
- expect(instanceSpy).toHaveBeenCalledWith(editorEl, expect.anything());
- });
+ it('does not create a model automatically if model is passed as `null`', () => {
+ mockDecorateInstance({
+ setModel,
+ });
+ editor.createInstance({ ...defaultArguments, model: null });
+ expect(modelSpy).not.toHaveBeenCalled();
+ expect(setModel).not.toHaveBeenCalled();
+ });
- it('with blobGlobalId, creates model with id in uri', () => {
- editor.createInstance({ el: editorEl, blobPath, blobContent, blobGlobalId });
+ it('initializes the instance on a supplied DOM node', () => {
+ editor.createInstance({ el: editorEl });
- expect(modelSpy).toHaveBeenCalledWith(
- blobContent,
- undefined,
- createUri(blobGlobalId, blobPath),
- );
- });
+ expect(editor.editorEl).not.toBe(null);
+ expect(instanceSpy).toHaveBeenCalledWith(editorEl, expect.anything());
+ });
- it('initializes instance with passed properties', () => {
- const instanceOptions = {
- foo: 'bar',
- };
- editor.createInstance({
- el: editorEl,
- ...instanceOptions,
+ it('with blobGlobalId, creates model with the id in uri', () => {
+ editor.createInstance(defaultArguments);
+
+ expect(modelSpy).toHaveBeenCalledWith(
+ blobContent,
+ undefined,
+ expect.objectContaining({
+ path: uriFilePath,
+ }),
+ );
+ });
+
+ it('initializes instance with passed properties', () => {
+ const instanceOptions = {
+ foo: 'bar',
+ };
+ editor.createInstance({
+ el: editorEl,
+ ...instanceOptions,
+ });
+ expect(instanceSpy).toHaveBeenCalledWith(
+ editorEl,
+ expect.objectContaining(instanceOptions),
+ );
+ });
+
+ it('disposes instance when the global editor is disposed', () => {
+ mockDecorateInstance({
+ dispose,
+ });
+ editor.createInstance(defaultArguments);
+
+ expect(dispose).not.toHaveBeenCalled();
+
+ editor.dispose();
+
+ expect(dispose).toHaveBeenCalled();
+ });
+
+ it("removes the disposed instance from the global editor's storage and disposes the associated model", () => {
+ mockModelReturn();
+ mockDecorateInstance({
+ setModel,
+ });
+ const instance = editor.createInstance(defaultArguments);
+
+ expect(editor.instances).toHaveLength(1);
+ expect(fakeModel.dispose).not.toHaveBeenCalled();
+
+ instance.dispose();
+
+ expect(editor.instances).toHaveLength(0);
+ expect(fakeModel.dispose).toHaveBeenCalled();
});
- expect(instanceSpy).toHaveBeenCalledWith(editorEl, expect.objectContaining(instanceOptions));
});
- it('disposes instance when the editor is disposed', () => {
- editor.createInstance({ el: editorEl, blobPath, blobContent, blobGlobalId });
+ describe('instance of the Diff Editor', () => {
+ beforeEach(() => {
+ instanceSpy = jest.spyOn(monacoEditor, 'createDiffEditor');
+ });
- expect(dispose).not.toHaveBeenCalled();
+ it('Diff Editor goes through the normal path of Code Editor just with the flag ON', () => {
+ const spy = jest.spyOn(editor, 'createInstance').mockImplementation(() => {});
+ editor.createDiffInstance();
+ expect(spy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ isDiff: true,
+ }),
+ );
+ });
- editor.dispose();
+ it('initializes the instance on a supplied DOM node', () => {
+ const wrongInstanceSpy = jest.spyOn(monacoEditor, 'create').mockImplementation(() => ({}));
+ editor.createDiffInstance({ ...defaultArguments, blobOriginalContent });
+
+ expect(editor.editorEl).not.toBe(null);
+ expect(wrongInstanceSpy).not.toHaveBeenCalled();
+ expect(instanceSpy).toHaveBeenCalledWith(editorEl, expect.anything());
+ });
+
+ it('creates correct model for the Diff Editor', () => {
+ const instance = editor.createDiffInstance({ ...defaultArguments, blobOriginalContent });
+ const getDiffModelValue = (model) => instance.getModel()[model].getValue();
+
+ expect(modelSpy).toHaveBeenCalledTimes(2);
+ expect(modelSpy.mock.calls[0]).toEqual([
+ blobContent,
+ undefined,
+ expect.objectContaining({
+ path: uriFilePath,
+ }),
+ ]);
+ expect(modelSpy.mock.calls[1]).toEqual([blobOriginalContent, 'markdown']);
+ expect(getDiffModelValue('original')).toBe(blobOriginalContent);
+ expect(getDiffModelValue('modified')).toBe(blobContent);
+ });
- expect(dispose).toHaveBeenCalled();
+ it('correctly disposes the diff editor model', () => {
+ const modifiedModel = fakeModel;
+ const originalModel = { ...fakeModel };
+ mockDecorateInstance({
+ getModel: jest.fn().mockReturnValue({
+ original: originalModel,
+ modified: modifiedModel,
+ }),
+ });
+
+ const instance = editor.createDiffInstance({ ...defaultArguments, blobOriginalContent });
+
+ expect(editor.instances).toHaveLength(1);
+ expect(originalModel.dispose).not.toHaveBeenCalled();
+ expect(modifiedModel.dispose).not.toHaveBeenCalled();
+
+ instance.dispose();
+
+ expect(editor.instances).toHaveLength(0);
+ expect(originalModel.dispose).toHaveBeenCalled();
+ expect(modifiedModel.dispose).toHaveBeenCalled();
+ });
});
});
@@ -127,16 +254,14 @@ describe('Base editor', () => {
editorEl2 = document.getElementById('editor2');
inst1Args = {
el: editorEl1,
- blobGlobalId,
};
inst2Args = {
el: editorEl2,
blobContent,
blobPath,
- blobGlobalId,
};
- editor = new Editor();
+ editor = new EditorLite();
instanceSpy = jest.spyOn(monacoEditor, 'create');
});
@@ -166,8 +291,20 @@ describe('Base editor', () => {
expect(model1).not.toEqual(model2);
});
+ it('does not create a new model if a model for the path & globalId combo already exists', () => {
+ const modelSpy = jest.spyOn(monacoEditor, 'createModel');
+ inst1 = editor.createInstance({ ...inst2Args, blobGlobalId });
+ inst2 = editor.createInstance({ ...inst2Args, el: editorEl1, blobGlobalId });
+
+ const model1 = inst1.getModel();
+ const model2 = inst2.getModel();
+
+ expect(modelSpy).toHaveBeenCalledTimes(1);
+ expect(model1).toBe(model2);
+ });
+
it('shares global editor options among all instances', () => {
- editor = new Editor({
+ editor = new EditorLite({
readOnly: true,
});
@@ -179,7 +316,7 @@ describe('Base editor', () => {
});
it('allows overriding editor options on the instance level', () => {
- editor = new Editor({
+ editor = new EditorLite({
readOnly: true,
});
inst1 = editor.createInstance({
@@ -200,6 +337,7 @@ describe('Base editor', () => {
expect(monacoEditor.getModels()).toHaveLength(2);
inst1.dispose();
+
expect(inst1.getModel()).toBe(null);
expect(inst2.getModel()).not.toBe(null);
expect(editor.instances).toHaveLength(1);
@@ -402,19 +540,20 @@ describe('Base editor', () => {
el: editorEl,
blobPath,
blobContent,
- blobGlobalId,
extensions,
});
};
beforeEach(() => {
- editorExtensionSpy = jest.spyOn(Editor, 'pushToImportsArray').mockImplementation((arr) => {
- arr.push(
- Promise.resolve({
- default: {},
- }),
- );
- });
+ editorExtensionSpy = jest
+ .spyOn(EditorLite, 'pushToImportsArray')
+ .mockImplementation((arr) => {
+ arr.push(
+ Promise.resolve({
+ default: {},
+ }),
+ );
+ });
});
it.each([undefined, [], [''], ''])(
@@ -446,15 +585,20 @@ describe('Base editor', () => {
expect(editorExtensionSpy).toHaveBeenCalledWith(expect.any(Array), expectation);
});
- it('emits editor-ready event after all extensions were applied', async () => {
+ it('emits EDITOR_READY_EVENT event after all extensions were applied', async () => {
const calls = [];
const eventSpy = jest.fn().mockImplementation(() => {
calls.push('event');
});
- const useSpy = jest.spyOn(editor, 'use').mockImplementation(() => {
+ const useSpy = jest.fn().mockImplementation(() => {
calls.push('use');
});
- editorEl.addEventListener('editor-ready', eventSpy);
+ jest.spyOn(EditorLite, 'convertMonacoToELInstance').mockImplementation((inst) => {
+ const decoratedInstance = inst;
+ decoratedInstance.use = useSpy;
+ return decoratedInstance;
+ });
+ editorEl.addEventListener(EDITOR_READY_EVENT, eventSpy);
instance = instanceConstructor('foo, bar');
await waitForPromises();
expect(useSpy.mock.calls).toHaveLength(2);
@@ -487,12 +631,6 @@ describe('Base editor', () => {
expect(inst1.alpha()).toEqual(alphaRes);
expect(inst2.alpha()).toEqual(alphaRes);
});
-
- it('extends specific instance if it has been passed', () => {
- editor.use(AlphaExt, inst2);
- expect(inst1.alpha).toBeUndefined();
- expect(inst2.alpha()).toEqual(alphaRes);
- });
});
});
@@ -526,7 +664,7 @@ describe('Base editor', () => {
it('sets default syntax highlighting theme', () => {
const expectedTheme = themes.find((t) => t.name === DEFAULT_THEME);
- editor = new Editor();
+ editor = new EditorLite();
expect(themeDefineSpy).toHaveBeenCalledWith(DEFAULT_THEME, expectedTheme.data);
expect(themeSetSpy).toHaveBeenCalledWith(DEFAULT_THEME);
@@ -538,7 +676,7 @@ describe('Base editor', () => {
expect(expectedTheme.name).not.toBe(DEFAULT_THEME);
window.gon.user_color_scheme = expectedTheme.name;
- editor = new Editor();
+ editor = new EditorLite();
expect(themeDefineSpy).toHaveBeenCalledWith(expectedTheme.name, expectedTheme.data);
expect(themeSetSpy).toHaveBeenCalledWith(expectedTheme.name);
@@ -549,7 +687,7 @@ describe('Base editor', () => {
const nonExistentTheme = { name };
window.gon.user_color_scheme = nonExistentTheme.name;
- editor = new Editor();
+ editor = new EditorLite();
expect(themeDefineSpy).not.toHaveBeenCalled();
expect(themeSetSpy).toHaveBeenCalledWith(DEFAULT_THEME);
diff --git a/spec/frontend/emoji/emoji_spec.js b/spec/frontend/emoji/index_spec.js
index feec445bc8d..1e6f5483160 100644
--- a/spec/frontend/emoji/emoji_spec.js
+++ b/spec/frontend/emoji/index_spec.js
@@ -1,6 +1,6 @@
+import { emojiFixtureMap, mockEmojiData, initEmojiMock } from 'helpers/emoji';
import { trimText } from 'helpers/text_helper';
-import { emojiFixtureMap, initEmojiMock, describeEmojiFields } from 'helpers/emoji';
-import { glEmojiTag, searchEmoji, getEmoji } from '~/emoji';
+import { glEmojiTag, searchEmoji, getEmojiInfo, sortEmoji } from '~/emoji';
import isEmojiUnicodeSupported, {
isFlagEmoji,
isRainbowFlagEmoji,
@@ -29,7 +29,7 @@ const emptySupportMap = {
1.1: false,
};
-describe('gl_emoji', () => {
+describe('emoji', () => {
let mock;
beforeEach(async () => {
@@ -43,7 +43,7 @@ describe('gl_emoji', () => {
describe('glEmojiTag', () => {
it('bomb emoji', () => {
const emojiKey = 'bomb';
- const markup = glEmojiTag(emojiFixtureMap[emojiKey].name);
+ const markup = glEmojiTag(emojiKey);
expect(trimText(markup)).toMatchInlineSnapshot(
`"<gl-emoji data-name=\\"bomb\\"></gl-emoji>"`,
@@ -52,7 +52,7 @@ describe('gl_emoji', () => {
it('bomb emoji with sprite fallback readiness', () => {
const emojiKey = 'bomb';
- const markup = glEmojiTag(emojiFixtureMap[emojiKey].name, {
+ const markup = glEmojiTag(emojiKey, {
sprite: true,
});
expect(trimText(markup)).toMatchInlineSnapshot(
@@ -352,125 +352,272 @@ describe('gl_emoji', () => {
});
});
- describe('getEmoji', () => {
- const { grey_question } = emojiFixtureMap;
-
- describe('when query is undefined', () => {
- it('should return null by default', () => {
- expect(getEmoji()).toBe(null);
- });
-
- it('should return fallback emoji when fallback is true', () => {
- expect(getEmoji(undefined, true).name).toEqual(grey_question.name);
- });
+ describe('getEmojiInfo', () => {
+ it.each(['atom', 'five', 'black_heart'])("should return a correct emoji for '%s'", (name) => {
+ expect(getEmojiInfo(name)).toEqual(mockEmojiData[name]);
});
- });
-
- describe('searchEmoji', () => {
- const { atom, grey_question } = emojiFixtureMap;
- const search = (query, opts) => searchEmoji(query, opts).map(({ name }) => name);
- const mangle = (str) => str.slice(0, 1) + str.slice(-1);
- const partial = (str) => str.slice(0, 2);
-
- describe('with default options', () => {
- const subject = (query) => search(query);
-
- describeEmojiFields('with $field', ({ accessor }) => {
- it(`should match by lower case: ${accessor(atom)}`, () => {
- expect(subject(accessor(atom))).toContain(atom.name);
- });
-
- it(`should match by upper case: ${accessor(atom).toUpperCase()}`, () => {
- expect(subject(accessor(atom).toUpperCase())).toContain(atom.name);
- });
-
- it(`should not match by partial: ${mangle(accessor(atom))}`, () => {
- expect(subject(mangle(accessor(atom)))).not.toContain(atom.name);
- });
- });
-
- it(`should match by unicode value: ${atom.moji}`, () => {
- expect(subject(atom.moji)).toContain(atom.name);
- });
-
- it('should not return a fallback value', () => {
- expect(subject('foo bar baz')).toHaveLength(0);
- });
- it('should not return a fallback value when query is falsey', () => {
- expect(subject()).toHaveLength(0);
- });
+ it('should return fallback emoji by default', () => {
+ expect(getEmojiInfo('atjs')).toEqual(mockEmojiData.grey_question);
});
- describe('with fuzzy match', () => {
- const subject = (query) => search(query, { match: 'fuzzy' });
-
- describeEmojiFields('with $field', ({ accessor }) => {
- it(`should match by lower case: ${accessor(atom)}`, () => {
- expect(subject(accessor(atom))).toContain(atom.name);
- });
-
- it(`should match by upper case: ${accessor(atom).toUpperCase()}`, () => {
- expect(subject(accessor(atom).toUpperCase())).toContain(atom.name);
- });
-
- it(`should match by partial: ${mangle(accessor(atom))}`, () => {
- expect(subject(mangle(accessor(atom)))).toContain(atom.name);
- });
- });
+ it('should return null when fallback is false', () => {
+ expect(getEmojiInfo('atjs', false)).toBe(null);
});
- describe('with contains match', () => {
- const subject = (query) => search(query, { match: 'contains' });
-
- describeEmojiFields('with $field', ({ accessor }) => {
- it(`should match by lower case: ${accessor(atom)}`, () => {
- expect(subject(accessor(atom))).toContain(atom.name);
- });
-
- it(`should match by upper case: ${accessor(atom).toUpperCase()}`, () => {
- expect(subject(accessor(atom).toUpperCase())).toContain(atom.name);
- });
-
- it(`should match by partial: ${partial(accessor(atom))}`, () => {
- expect(subject(partial(accessor(atom)))).toContain(atom.name);
- });
-
- it(`should not match by mangled: ${mangle(accessor(atom))}`, () => {
- expect(subject(mangle(accessor(atom)))).not.toContain(atom.name);
- });
+ describe('when query is undefined', () => {
+ it('should return fallback emoji by default', () => {
+ expect(getEmojiInfo()).toEqual(mockEmojiData.grey_question);
});
- });
- describe('with fallback', () => {
- const subject = (query) => search(query, { fallback: true });
-
- it.each`
- query
- ${'foo bar baz'} | ${undefined}
- `('should return a fallback value when given $query', ({ query }) => {
- expect(subject(query)).toContain(grey_question.name);
+ it('should return null when fallback is false', () => {
+ expect(getEmojiInfo(undefined, false)).toBe(null);
});
});
+ });
- describe('with name and alias fields', () => {
- const subject = (query) => search(query, { fields: ['name', 'alias'] });
-
- it(`should match by name: ${atom.name}`, () => {
- expect(subject(atom.name)).toContain(atom.name);
+ describe('searchEmoji', () => {
+ const emojiFixture = Object.keys(mockEmojiData).reduce((acc, k) => {
+ const { name, e, u, d } = mockEmojiData[k];
+ acc[k] = { name, e, u, d };
+
+ return acc;
+ }, {});
+
+ it.each([undefined, null, ''])("should return all emoji when the input is '%s'", (input) => {
+ const search = searchEmoji(input);
+
+ const expected = [
+ 'atom',
+ 'bomb',
+ 'construction_worker_tone5',
+ 'five',
+ 'grey_question',
+ 'black_heart',
+ 'heart',
+ 'custard',
+ 'star',
+ ].map((name) => {
+ return {
+ emoji: emojiFixture[name],
+ field: 'd',
+ fieldValue: emojiFixture[name].d,
+ score: 0,
+ };
});
- it(`should match by alias: ${atom.aliases[0]}`, () => {
- expect(subject(atom.aliases[0])).toContain(atom.name);
+ expect(search).toEqual(expected);
+ });
+
+ it.each([
+ [
+ 'searching by unicode value',
+ '⚛',
+ [
+ {
+ name: 'atom',
+ field: 'e',
+ fieldValue: 'atom',
+ score: 0,
+ },
+ ],
+ ],
+ [
+ 'searching by partial alias',
+ '_symbol',
+ [
+ {
+ name: 'atom',
+ field: 'alias',
+ fieldValue: 'atom_symbol',
+ score: 4,
+ },
+ ],
+ ],
+ [
+ 'searching by full alias',
+ 'atom_symbol',
+ [
+ {
+ name: 'atom',
+ field: 'alias',
+ fieldValue: 'atom_symbol',
+ score: 0,
+ },
+ ],
+ ],
+ ])('should return a correct result when %s', (_, query, searchResult) => {
+ const expected = searchResult.map((item) => {
+ const { field, score, fieldValue, name } = item;
+
+ return {
+ emoji: emojiFixture[name],
+ field,
+ fieldValue,
+ score,
+ };
});
- it(`should not match by description: ${atom.description}`, () => {
- expect(subject(atom.description)).not.toContain(atom.name);
+ expect(searchEmoji(query)).toEqual(expected);
+ });
+
+ it.each([
+ ['searching with a non-existing emoji name', 'asdf', []],
+ [
+ 'searching by full name',
+ 'atom',
+ [
+ {
+ name: 'atom',
+ field: 'd',
+ score: 0,
+ },
+ ],
+ ],
+
+ [
+ 'searching by full description',
+ 'atom symbol',
+ [
+ {
+ name: 'atom',
+ field: 'd',
+ score: 0,
+ },
+ ],
+ ],
+
+ [
+ 'searching by partial name',
+ 'question',
+ [
+ {
+ name: 'grey_question',
+ field: 'name',
+ score: 5,
+ },
+ ],
+ ],
+ [
+ 'searching by partial description',
+ 'ment',
+ [
+ {
+ name: 'grey_question',
+ field: 'd',
+ score: 24,
+ },
+ ],
+ ],
+ [
+ 'searching with query "heart"',
+ 'heart',
+ [
+ {
+ name: 'black_heart',
+ field: 'd',
+ score: 6,
+ },
+ {
+ name: 'heart',
+ field: 'name',
+ score: 0,
+ },
+ ],
+ ],
+ [
+ 'searching with query "HEART"',
+ 'HEART',
+ [
+ {
+ name: 'black_heart',
+ field: 'd',
+ score: 6,
+ },
+ {
+ name: 'heart',
+ field: 'name',
+ score: 0,
+ },
+ ],
+ ],
+ [
+ 'searching with query "star"',
+ 'star',
+ [
+ {
+ name: 'custard',
+ field: 'd',
+ score: 2,
+ },
+ {
+ name: 'star',
+ field: 'name',
+ score: 0,
+ },
+ ],
+ ],
+ ])('should return a correct result when %s', (_, query, searchResult) => {
+ const expected = searchResult.map((item) => {
+ const { field, score, name } = item;
+
+ return {
+ emoji: emojiFixture[name],
+ field,
+ fieldValue: emojiFixture[name][field],
+ score,
+ };
});
- it(`should not match by unicode value: ${atom.moji}`, () => {
- expect(subject(atom.moji)).not.toContain(atom.name);
- });
+ expect(searchEmoji(query)).toEqual(expected);
+ });
+ });
+
+ describe('sortEmoji', () => {
+ const testCases = [
+ [
+ 'should correctly sort by score',
+ [
+ { score: 10, fieldValue: '', emoji: { name: 'a' } },
+ { score: 5, fieldValue: '', emoji: { name: 'b' } },
+ { score: 0, fieldValue: '', emoji: { name: 'c' } },
+ ],
+ [
+ { score: 0, fieldValue: '', emoji: { name: 'c' } },
+ { score: 5, fieldValue: '', emoji: { name: 'b' } },
+ { score: 10, fieldValue: '', emoji: { name: 'a' } },
+ ],
+ ],
+ [
+ 'should correctly sort by fieldValue',
+ [
+ { score: 0, fieldValue: 'y', emoji: { name: 'b' } },
+ { score: 0, fieldValue: 'x', emoji: { name: 'a' } },
+ { score: 0, fieldValue: 'z', emoji: { name: 'c' } },
+ ],
+ [
+ { score: 0, fieldValue: 'x', emoji: { name: 'a' } },
+ { score: 0, fieldValue: 'y', emoji: { name: 'b' } },
+ { score: 0, fieldValue: 'z', emoji: { name: 'c' } },
+ ],
+ ],
+ [
+ 'should correctly sort by score and then by fieldValue (in order)',
+ [
+ { score: 5, fieldValue: 'y', emoji: { name: 'c' } },
+ { score: 0, fieldValue: 'z', emoji: { name: 'a' } },
+ { score: 5, fieldValue: 'x', emoji: { name: 'b' } },
+ ],
+ [
+ { score: 0, fieldValue: 'z', emoji: { name: 'a' } },
+ { score: 5, fieldValue: 'x', emoji: { name: 'b' } },
+ { score: 5, fieldValue: 'y', emoji: { name: 'c' } },
+ ],
+ ],
+ ];
+
+ it.each(testCases)('%s', (_, scoredItems, expected) => {
+ expect(sortEmoji(scoredItems)).toEqual(expected);
});
});
});
diff --git a/spec/frontend/environment.js b/spec/frontend/environment.js
index c055702d832..d1bc11538a3 100644
--- a/spec/frontend/environment.js
+++ b/spec/frontend/environment.js
@@ -1,8 +1,12 @@
/* eslint-disable import/no-commonjs, max-classes-per-file */
const path = require('path');
-const { ErrorWithStack } = require('jest-util');
const JSDOMEnvironment = require('jest-environment-jsdom');
+const { ErrorWithStack } = require('jest-util');
+const {
+ setGlobalDateToFakeDate,
+ setGlobalDateToRealDate,
+} = require('./__helpers__/fake_date/fake_date');
const { TEST_HOST } = require('./__helpers__/test_constants');
const ROOT_PATH = path.resolve(__dirname, '../..');
@@ -12,6 +16,10 @@ class CustomEnvironment extends JSDOMEnvironment {
// Setup testURL so that window.location is setup properly
super({ ...config, testURL: TEST_HOST }, context);
+ // Fake the `Date` for `jsdom` which fixes things like document.cookie
+ // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39496#note_503084332
+ setGlobalDateToFakeDate();
+
Object.assign(context.console, {
error(...args) {
throw new ErrorWithStack(
@@ -69,6 +77,9 @@ class CustomEnvironment extends JSDOMEnvironment {
}
async teardown() {
+ // Reset `Date` so that Jest can report timing accurately *roll eyes*...
+ setGlobalDateToRealDate();
+
await new Promise(setImmediate);
if (this.rejectedPromises.length > 0) {
diff --git a/spec/frontend/environments/canary_ingress_spec.js b/spec/frontend/environments/canary_ingress_spec.js
index 3dd67de1369..6c7a786e652 100644
--- a/spec/frontend/environments/canary_ingress_spec.js
+++ b/spec/frontend/environments/canary_ingress_spec.js
@@ -1,8 +1,8 @@
-import { mount } from '@vue/test-utils';
import { GlDropdownItem } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import { CANARY_UPDATE_MODAL } from '~/environments/constants';
import CanaryIngress from '~/environments/components/canary_ingress.vue';
+import { CANARY_UPDATE_MODAL } from '~/environments/constants';
describe('/environments/components/canary_ingress.vue', () => {
let wrapper;
diff --git a/spec/frontend/environments/canary_update_modal_spec.js b/spec/frontend/environments/canary_update_modal_spec.js
index d0b97cf2eda..c7129ee1320 100644
--- a/spec/frontend/environments/canary_update_modal_spec.js
+++ b/spec/frontend/environments/canary_update_modal_spec.js
@@ -1,5 +1,5 @@
-import { mount } from '@vue/test-utils';
import { GlAlert, GlModal } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import CanaryUpdateModal from '~/environments/components/canary_update_modal.vue';
import updateCanaryIngress from '~/environments/graphql/mutations/update_canary_ingress.mutation.graphql';
diff --git a/spec/frontend/environments/confirm_rollback_modal_spec.js b/spec/frontend/environments/confirm_rollback_modal_spec.js
index a1a22274e8f..8fb53579f96 100644
--- a/spec/frontend/environments/confirm_rollback_modal_spec.js
+++ b/spec/frontend/environments/confirm_rollback_modal_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import ConfirmRollbackModal from '~/environments/components/confirm_rollback_modal.vue';
import eventHub from '~/environments/event_hub';
diff --git a/spec/frontend/environments/deploy_board_component_spec.js b/spec/frontend/environments/deploy_board_component_spec.js
index dbef03f99d8..53220341a62 100644
--- a/spec/frontend/environments/deploy_board_component_spec.js
+++ b/spec/frontend/environments/deploy_board_component_spec.js
@@ -1,8 +1,8 @@
import { GlTooltip, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import Vue from 'vue';
-import DeployBoard from '~/environments/components/deploy_board.vue';
import CanaryIngress from '~/environments/components/canary_ingress.vue';
+import DeployBoard from '~/environments/components/deploy_board.vue';
import { deployBoardMockData, environment } from './mock_data';
const logsPath = `gitlab-org/gitlab-test/-/logs?environment_name=${environment.name}`;
diff --git a/spec/frontend/environments/enable_review_app_modal_spec.js b/spec/frontend/environments/enable_review_app_modal_spec.js
index 7ea49a6e1d0..f5063cff620 100644
--- a/spec/frontend/environments/enable_review_app_modal_spec.js
+++ b/spec/frontend/environments/enable_review_app_modal_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
import EnableReviewAppButton from '~/environments/components/enable_review_app_modal.vue';
+import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
describe('Enable Review App Button', () => {
let wrapper;
diff --git a/spec/frontend/environments/environment_actions_spec.js b/spec/frontend/environments/environment_actions_spec.js
index 875a01c07ea..db78a6b0cdd 100644
--- a/spec/frontend/environments/environment_actions_spec.js
+++ b/spec/frontend/environments/environment_actions_spec.js
@@ -1,9 +1,9 @@
+import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
-import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import eventHub from '~/environments/event_hub';
import EnvironmentActions from '~/environments/components/environment_actions.vue';
+import eventHub from '~/environments/event_hub';
const scheduledJobAction = {
name: 'scheduled action',
diff --git a/spec/frontend/environments/environment_item_spec.js b/spec/frontend/environments/environment_item_spec.js
index bc692352103..09ab1223fd1 100644
--- a/spec/frontend/environments/environment_item_spec.js
+++ b/spec/frontend/environments/environment_item_spec.js
@@ -1,9 +1,9 @@
-import { cloneDeep } from 'lodash';
import { mount } from '@vue/test-utils';
+import { cloneDeep } from 'lodash';
import { format } from 'timeago.js';
+import DeleteComponent from '~/environments/components/environment_delete.vue';
import EnvironmentItem from '~/environments/components/environment_item.vue';
import PinComponent from '~/environments/components/environment_pin.vue';
-import DeleteComponent from '~/environments/components/environment_delete.vue';
import { differenceInMilliseconds } from '~/lib/utils/datetime_utility';
import { environment, folder, tableData } from './mock_data';
diff --git a/spec/frontend/environments/environment_monitoring_spec.js b/spec/frontend/environments/environment_monitoring_spec.js
index bbd49c04fb6..3a53b57c3c6 100644
--- a/spec/frontend/environments/environment_monitoring_spec.js
+++ b/spec/frontend/environments/environment_monitoring_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import MonitoringComponent from '~/environments/components/environment_monitoring.vue';
describe('Monitoring Component', () => {
diff --git a/spec/frontend/environments/environment_pin_spec.js b/spec/frontend/environments/environment_pin_spec.js
index f48091adb44..5cdd52294b6 100644
--- a/spec/frontend/environments/environment_pin_spec.js
+++ b/spec/frontend/environments/environment_pin_spec.js
@@ -1,7 +1,7 @@
-import { shallowMount } from '@vue/test-utils';
import { GlButton, GlIcon } from '@gitlab/ui';
-import eventHub from '~/environments/event_hub';
+import { shallowMount } from '@vue/test-utils';
import PinComponent from '~/environments/components/environment_pin.vue';
+import eventHub from '~/environments/event_hub';
describe('Pin Component', () => {
let wrapper;
diff --git a/spec/frontend/environments/environment_rollback_spec.js b/spec/frontend/environments/environment_rollback_spec.js
index fb62a096c3d..b6c3d436c18 100644
--- a/spec/frontend/environments/environment_rollback_spec.js
+++ b/spec/frontend/environments/environment_rollback_spec.js
@@ -1,7 +1,7 @@
-import { shallowMount, mount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
-import eventHub from '~/environments/event_hub';
+import { shallowMount, mount } from '@vue/test-utils';
import RollbackComponent from '~/environments/components/environment_rollback.vue';
+import eventHub from '~/environments/event_hub';
describe('Rollback Component', () => {
const retryUrl = 'https://gitlab.com/retry';
diff --git a/spec/frontend/environments/environment_stop_spec.js b/spec/frontend/environments/environment_stop_spec.js
index 1865403cdc4..dff444b79f3 100644
--- a/spec/frontend/environments/environment_stop_spec.js
+++ b/spec/frontend/environments/environment_stop_spec.js
@@ -1,6 +1,6 @@
-import $ from 'jquery';
-import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import $ from 'jquery';
import StopComponent from '~/environments/components/environment_stop.vue';
import eventHub from '~/environments/event_hub';
diff --git a/spec/frontend/environments/environment_table_spec.js b/spec/frontend/environments/environment_table_spec.js
index daef35bcf99..863c4526bb9 100644
--- a/spec/frontend/environments/environment_table_spec.js
+++ b/spec/frontend/environments/environment_table_spec.js
@@ -1,8 +1,8 @@
import { mount } from '@vue/test-utils';
+import CanaryUpdateModal from '~/environments/components/canary_update_modal.vue';
+import DeployBoard from '~/environments/components/deploy_board.vue';
import EnvironmentTable from '~/environments/components/environments_table.vue';
import eventHub from '~/environments/event_hub';
-import DeployBoard from '~/environments/components/deploy_board.vue';
-import CanaryUpdateModal from '~/environments/components/canary_update_modal.vue';
import { folder, deployBoardMockData } from './mock_data';
const eeOnlyProps = {
diff --git a/spec/frontend/environments/environments_app_spec.js b/spec/frontend/environments/environments_app_spec.js
index d6614e2fd2b..50d84b19ce8 100644
--- a/spec/frontend/environments/environments_app_spec.js
+++ b/spec/frontend/environments/environments_app_spec.js
@@ -1,11 +1,11 @@
import { mount, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import EnableReviewAppModal from '~/environments/components/enable_review_app_modal.vue';
import Container from '~/environments/components/container.vue';
+import DeployBoard from '~/environments/components/deploy_board.vue';
import EmptyState from '~/environments/components/empty_state.vue';
+import EnableReviewAppModal from '~/environments/components/enable_review_app_modal.vue';
import EnvironmentsApp from '~/environments/components/environments_app.vue';
-import DeployBoard from '~/environments/components/deploy_board.vue';
import axios from '~/lib/utils/axios_utils';
import { environment, folder } from './mock_data';
diff --git a/spec/frontend/error_tracking/components/error_details_spec.js b/spec/frontend/error_tracking/components/error_details_spec.js
index 0b9e0008ed7..b469a855d23 100644
--- a/spec/frontend/error_tracking/components/error_details_spec.js
+++ b/spec/frontend/error_tracking/components/error_details_spec.js
@@ -1,5 +1,3 @@
-import { createLocalVue, shallowMount } from '@vue/test-utils';
-import Vuex from 'vuex';
import {
GlButton,
GlLoadingIcon,
@@ -9,21 +7,23 @@ import {
GlAlert,
GlSprintf,
} from '@gitlab/ui';
-import { __ } from '~/locale';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
-import Stacktrace from '~/error_tracking/components/stacktrace.vue';
-import ErrorDetails from '~/error_tracking/components/error_details.vue';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import Vuex from 'vuex';
import {
severityLevel,
severityLevelVariant,
errorStatus,
} from '~/error_tracking/components/constants';
-import Tracking from '~/tracking';
+import ErrorDetails from '~/error_tracking/components/error_details.vue';
+import Stacktrace from '~/error_tracking/components/stacktrace.vue';
import {
trackClickErrorLinkToSentryOptions,
trackErrorDetailsViewsOptions,
trackErrorStatusUpdateOptions,
} from '~/error_tracking/utils';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+import { __ } from '~/locale';
+import Tracking from '~/tracking';
jest.mock('~/flash');
diff --git a/spec/frontend/error_tracking/components/error_tracking_actions_spec.js b/spec/frontend/error_tracking/components/error_tracking_actions_spec.js
index 05b3d2f1dec..e21c40423c3 100644
--- a/spec/frontend/error_tracking/components/error_tracking_actions_spec.js
+++ b/spec/frontend/error_tracking/components/error_tracking_actions_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import ErrorTrackingActions from '~/error_tracking/components/error_tracking_actions.vue';
describe('Error Tracking Actions', () => {
diff --git a/spec/frontend/error_tracking/components/error_tracking_list_spec.js b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
index da12237b1d9..c6ce236af01 100644
--- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js
+++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
@@ -1,12 +1,12 @@
+import { GlEmptyState, GlLoadingIcon, GlFormInput, GlPagination, GlDropdown } from '@gitlab/ui';
import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex';
-import { GlEmptyState, GlLoadingIcon, GlFormInput, GlPagination, GlDropdown } from '@gitlab/ui';
import stubChildren from 'helpers/stub_children';
-import ErrorTrackingList from '~/error_tracking/components/error_tracking_list.vue';
import ErrorTrackingActions from '~/error_tracking/components/error_tracking_actions.vue';
+import ErrorTrackingList from '~/error_tracking/components/error_tracking_list.vue';
import { trackErrorListViewsOptions, trackErrorStatusUpdateOptions } from '~/error_tracking/utils';
-import errorsList from './list_mock.json';
import Tracking from '~/tracking';
+import errorsList from './list_mock.json';
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -298,9 +298,7 @@ describe('ErrorTrackingList', () => {
});
it('shows empty state', () => {
- expect(wrapper.find('a').attributes('href')).toBe(
- '/help/user/project/operations/error_tracking.html',
- );
+ expect(wrapper.find(GlEmptyState).isVisible()).toBe(true);
});
});
diff --git a/spec/frontend/error_tracking/components/stacktrace_entry_spec.js b/spec/frontend/error_tracking/components/stacktrace_entry_spec.js
index 6df25ad6819..0b43167c19b 100644
--- a/spec/frontend/error_tracking/components/stacktrace_entry_spec.js
+++ b/spec/frontend/error_tracking/components/stacktrace_entry_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlSprintf, GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { trimText } from 'helpers/text_helper';
import StackTraceEntry from '~/error_tracking/components/stacktrace_entry.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
diff --git a/spec/frontend/error_tracking/store/actions_spec.js b/spec/frontend/error_tracking/store/actions_spec.js
index 34ad600af05..9d598344acd 100644
--- a/spec/frontend/error_tracking/store/actions_spec.js
+++ b/spec/frontend/error_tracking/store/actions_spec.js
@@ -1,9 +1,9 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import axios from '~/lib/utils/axios_utils';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
import * as actions from '~/error_tracking/store/actions';
import * as types from '~/error_tracking/store/mutation_types';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/flash.js');
diff --git a/spec/frontend/error_tracking/store/details/actions_spec.js b/spec/frontend/error_tracking/store/details/actions_spec.js
index 152ecde6985..0c19dce7bad 100644
--- a/spec/frontend/error_tracking/store/details/actions_spec.js
+++ b/spec/frontend/error_tracking/store/details/actions_spec.js
@@ -1,9 +1,9 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import axios from '~/lib/utils/axios_utils';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
import * as actions from '~/error_tracking/store/details/actions';
import * as types from '~/error_tracking/store/details/mutation_types';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
import Poll from '~/lib/utils/poll';
let mockedAdapter;
diff --git a/spec/frontend/error_tracking/store/list/actions_spec.js b/spec/frontend/error_tracking/store/list/actions_spec.js
index a93608fe70d..39481a8576f 100644
--- a/spec/frontend/error_tracking/store/list/actions_spec.js
+++ b/spec/frontend/error_tracking/store/list/actions_spec.js
@@ -1,10 +1,10 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import axios from '~/lib/utils/axios_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
import * as actions from '~/error_tracking/store/list/actions';
import * as types from '~/error_tracking/store/list/mutation_types';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import httpStatusCodes from '~/lib/utils/http_status';
jest.mock('~/flash.js');
diff --git a/spec/frontend/error_tracking/store/list/mutation_spec.js b/spec/frontend/error_tracking/store/list/mutation_spec.js
index a326a6c55c0..d28d3ecc79f 100644
--- a/spec/frontend/error_tracking/store/list/mutation_spec.js
+++ b/spec/frontend/error_tracking/store/list/mutation_spec.js
@@ -1,6 +1,6 @@
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
-import mutations from '~/error_tracking/store/list/mutations';
import * as types from '~/error_tracking/store/list/mutation_types';
+import mutations from '~/error_tracking/store/list/mutations';
const ADD_RECENT_SEARCH = mutations[types.ADD_RECENT_SEARCH];
const CLEAR_RECENT_SEARCHES = mutations[types.CLEAR_RECENT_SEARCHES];
diff --git a/spec/frontend/error_tracking_settings/components/app_spec.js b/spec/frontend/error_tracking_settings/components/app_spec.js
index 5c3efa24551..e0be81b3899 100644
--- a/spec/frontend/error_tracking_settings/components/app_spec.js
+++ b/spec/frontend/error_tracking_settings/components/app_spec.js
@@ -1,5 +1,5 @@
-import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
+import Vuex from 'vuex';
import { TEST_HOST } from 'helpers/test_constants';
import ErrorTrackingSettings from '~/error_tracking_settings/components/app.vue';
import ErrorTrackingForm from '~/error_tracking_settings/components/error_tracking_form.vue';
diff --git a/spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js b/spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js
index e2a8b57f555..7ebaf0c3f2a 100644
--- a/spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js
+++ b/spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js
@@ -1,6 +1,6 @@
-import Vuex from 'vuex';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlFormInput, GlButton } from '@gitlab/ui';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import Vuex from 'vuex';
import ErrorTrackingForm from '~/error_tracking_settings/components/error_tracking_form.vue';
import createStore from '~/error_tracking_settings/store';
import { defaultProps } from '../mock';
diff --git a/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js b/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js
index d924f895da8..79518a487d4 100644
--- a/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js
+++ b/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js
@@ -1,7 +1,7 @@
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
import { pick, clone } from 'lodash';
import Vuex from 'vuex';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import ProjectDropdown from '~/error_tracking_settings/components/project_dropdown.vue';
import { defaultProps, projectList, staleProject } from '../mock';
diff --git a/spec/frontend/error_tracking_settings/store/actions_spec.js b/spec/frontend/error_tracking_settings/store/actions_spec.js
index 216bf8011e8..281db7d9686 100644
--- a/spec/frontend/error_tracking_settings/store/actions_spec.js
+++ b/spec/frontend/error_tracking_settings/store/actions_spec.js
@@ -1,12 +1,12 @@
import MockAdapter from 'axios-mock-adapter';
-import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'helpers/test_constants';
-import axios from '~/lib/utils/axios_utils';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import { refreshCurrentPage } from '~/lib/utils/url_utility';
+import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/error_tracking_settings/store/actions';
import * as types from '~/error_tracking_settings/store/mutation_types';
import defaultState from '~/error_tracking_settings/store/state';
+import axios from '~/lib/utils/axios_utils';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { refreshCurrentPage } from '~/lib/utils/url_utility';
import { projectList } from '../mock';
jest.mock('~/lib/utils/url_utility');
diff --git a/spec/frontend/error_tracking_settings/store/mutation_spec.js b/spec/frontend/error_tracking_settings/store/mutation_spec.js
index fa188462c3f..78fd56904b3 100644
--- a/spec/frontend/error_tracking_settings/store/mutation_spec.js
+++ b/spec/frontend/error_tracking_settings/store/mutation_spec.js
@@ -1,7 +1,7 @@
import { TEST_HOST } from 'helpers/test_constants';
+import * as types from '~/error_tracking_settings/store/mutation_types';
import mutations from '~/error_tracking_settings/store/mutations';
import defaultState from '~/error_tracking_settings/store/state';
-import * as types from '~/error_tracking_settings/store/mutation_types';
import {
initialEmptyState,
initialPopulatedState,
diff --git a/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js b/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js
index 21b894ccbef..84e71ffd204 100644
--- a/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js
+++ b/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlModal, GlSprintf, GlAlert } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import Component from '~/feature_flags/components/configure_feature_flags_modal.vue';
describe('Configure Feature Flags Modal', () => {
diff --git a/spec/frontend/feature_flags/components/edit_feature_flag_spec.js b/spec/frontend/feature_flags/components/edit_feature_flag_spec.js
index a754c682356..e2717b98ea9 100644
--- a/spec/frontend/feature_flags/components/edit_feature_flag_spec.js
+++ b/spec/frontend/feature_flags/components/edit_feature_flag_spec.js
@@ -1,13 +1,13 @@
-import Vuex from 'vuex';
+import { GlToggle, GlAlert } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import { GlToggle, GlAlert } from '@gitlab/ui';
-import { TEST_HOST } from 'spec/test_constants';
+import Vuex from 'vuex';
import { mockTracking } from 'helpers/tracking_helper';
-import { LEGACY_FLAG, NEW_VERSION_FLAG } from '~/feature_flags/constants';
+import { TEST_HOST } from 'spec/test_constants';
+import EditFeatureFlag from '~/feature_flags/components/edit_feature_flag.vue';
import Form from '~/feature_flags/components/form.vue';
+import { LEGACY_FLAG, NEW_VERSION_FLAG } from '~/feature_flags/constants';
import createStore from '~/feature_flags/store/edit';
-import EditFeatureFlag from '~/feature_flags/components/edit_feature_flag.vue';
import axios from '~/lib/utils/axios_utils';
const localVue = createLocalVue();
@@ -75,6 +75,8 @@ describe('Edit feature flag form', () => {
});
const findAlert = () => wrapper.find(GlAlert);
+ const findWarningGlAlert = () =>
+ wrapper.findAll(GlAlert).filter((c) => c.props('variant') === 'warning');
it('should display the iid', () => {
expect(wrapper.find('h3').text()).toContain('^5');
@@ -88,7 +90,7 @@ describe('Edit feature flag form', () => {
expect(wrapper.find(GlToggle).props('value')).toBe(true);
});
- it('should not alert users that feature flags are changing soon', () => {
+ it('should alert users the flag is read only', () => {
expect(findAlert().text()).toContain('GitLab is moving to a new way of managing feature flags');
});
@@ -96,8 +98,9 @@ describe('Edit feature flag form', () => {
it('should render the error', () => {
store.dispatch('receiveUpdateFeatureFlagError', { message: ['The name is required'] });
return wrapper.vm.$nextTick(() => {
- expect(wrapper.find('.alert-danger').exists()).toEqual(true);
- expect(wrapper.find('.alert-danger').text()).toContain('The name is required');
+ const warningGlAlert = findWarningGlAlert();
+ expect(warningGlAlert.at(1).exists()).toEqual(true);
+ expect(warningGlAlert.at(1).text()).toContain('The name is required');
});
});
});
diff --git a/spec/frontend/feature_flags/components/environments_dropdown_spec.js b/spec/frontend/feature_flags/components/environments_dropdown_spec.js
index 042fccaa369..9194db3a182 100644
--- a/spec/frontend/feature_flags/components/environments_dropdown_spec.js
+++ b/spec/frontend/feature_flags/components/environments_dropdown_spec.js
@@ -1,8 +1,8 @@
-import MockAdapter from 'axios-mock-adapter';
-import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon, GlButton, GlSearchBoxByType } from '@gitlab/ui';
-import { TEST_HOST } from 'spec/test_constants';
+import { shallowMount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
+import { TEST_HOST } from 'spec/test_constants';
import EnvironmentsDropdown from '~/feature_flags/components/environments_dropdown.vue';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
diff --git a/spec/frontend/feature_flags/components/feature_flags_spec.js b/spec/frontend/feature_flags/components/feature_flags_spec.js
index 8242d667d2e..b519aab0dc4 100644
--- a/spec/frontend/feature_flags/components/feature_flags_spec.js
+++ b/spec/frontend/feature_flags/components/feature_flags_spec.js
@@ -1,18 +1,18 @@
+import { GlAlert, GlEmptyState, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
-import Vuex from 'vuex';
import MockAdapter from 'axios-mock-adapter';
-import { GlAlert, GlEmptyState, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
+import Vuex from 'vuex';
import { TEST_HOST } from 'spec/test_constants';
import Api from '~/api';
-import createStore from '~/feature_flags/store/index';
-import FeatureFlagsTab from '~/feature_flags/components/feature_flags_tab.vue';
+import ConfigureFeatureFlagsModal from '~/feature_flags/components/configure_feature_flags_modal.vue';
import FeatureFlagsComponent from '~/feature_flags/components/feature_flags.vue';
+import FeatureFlagsTab from '~/feature_flags/components/feature_flags_tab.vue';
import FeatureFlagsTable from '~/feature_flags/components/feature_flags_table.vue';
import UserListsTable from '~/feature_flags/components/user_lists_table.vue';
-import ConfigureFeatureFlagsModal from '~/feature_flags/components/configure_feature_flags_modal.vue';
import { FEATURE_FLAG_SCOPE, USER_LIST_SCOPE } from '~/feature_flags/constants';
-import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
+import createStore from '~/feature_flags/store/index';
import axios from '~/lib/utils/axios_utils';
+import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import { getRequestData, userList } from '../mock_data';
const localVue = createLocalVue();
diff --git a/spec/frontend/feature_flags/components/feature_flags_tab_spec.js b/spec/frontend/feature_flags/components/feature_flags_tab_spec.js
index 23cc7045d1f..c2170e8a768 100644
--- a/spec/frontend/feature_flags/components/feature_flags_tab_spec.js
+++ b/spec/frontend/feature_flags/components/feature_flags_tab_spec.js
@@ -1,5 +1,5 @@
-import { mount } from '@vue/test-utils';
import { GlAlert, GlBadge, GlEmptyState, GlLink, GlLoadingIcon, GlTabs } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import FeatureFlagsTab from '~/feature_flags/components/feature_flags_tab.vue';
const DEFAULT_PROPS = {
diff --git a/spec/frontend/feature_flags/components/feature_flags_table_spec.js b/spec/frontend/feature_flags/components/feature_flags_table_spec.js
index 8881cfae88d..8f4d39d4a11 100644
--- a/spec/frontend/feature_flags/components/feature_flags_table_spec.js
+++ b/spec/frontend/feature_flags/components/feature_flags_table_spec.js
@@ -1,7 +1,8 @@
-import { shallowMount } from '@vue/test-utils';
import { GlToggle, GlBadge } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { trimText } from 'helpers/text_helper';
import { mockTracking } from 'helpers/tracking_helper';
+import FeatureFlagsTable from '~/feature_flags/components/feature_flags_table.vue';
import {
ROLLOUT_STRATEGY_ALL_USERS,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
@@ -11,7 +12,6 @@ import {
LEGACY_FLAG,
DEFAULT_PERCENT_ROLLOUT,
} from '~/feature_flags/constants';
-import FeatureFlagsTable from '~/feature_flags/components/feature_flags_table.vue';
const getDefaultProps = () => ({
featureFlags: [
diff --git a/spec/frontend/feature_flags/components/form_spec.js b/spec/frontend/feature_flags/components/form_spec.js
index 3a057aedde9..a05e23a4250 100644
--- a/spec/frontend/feature_flags/components/form_spec.js
+++ b/spec/frontend/feature_flags/components/form_spec.js
@@ -1,9 +1,10 @@
-import { uniqueId } from 'lodash';
+import { GlFormTextarea, GlFormCheckbox, GlButton, GlToggle } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { GlFormTextarea, GlFormCheckbox, GlButton } from '@gitlab/ui';
+import { uniqueId } from 'lodash';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import Api from '~/api';
-import Form from '~/feature_flags/components/form.vue';
import EnvironmentsDropdown from '~/feature_flags/components/environments_dropdown.vue';
+import Form from '~/feature_flags/components/form.vue';
import Strategy from '~/feature_flags/components/strategy.vue';
import {
ROLLOUT_STRATEGY_ALL_USERS,
@@ -14,7 +15,6 @@ import {
NEW_VERSION_FLAG,
} from '~/feature_flags/constants';
import RelatedIssuesRoot from '~/related_issues/components/related_issues_root.vue';
-import ToggleButton from '~/vue_shared/components/toggle_button.vue';
import { featureFlag, userList, allUsersStrategy } from '../mock_data';
jest.mock('~/api.js');
@@ -35,14 +35,19 @@ describe('feature flag form', () => {
},
};
+ const findAddNewScopeRow = () => wrapper.findByTestId('add-new-scope');
+ const findGlToggle = () => wrapper.find(GlToggle);
+
const factory = (props = {}, provide = {}) => {
- wrapper = shallowMount(Form, {
- propsData: { ...requiredProps, ...props },
- provide: {
- ...requiredInjections,
- ...provide,
- },
- });
+ wrapper = extendedWrapper(
+ shallowMount(Form, {
+ propsData: { ...requiredProps, ...props },
+ provide: {
+ ...requiredInjections,
+ ...provide,
+ },
+ }),
+ );
};
beforeEach(() => {
@@ -102,13 +107,13 @@ describe('feature flag form', () => {
});
it('should render scopes table with a new row ', () => {
- expect(wrapper.find('.js-add-new-scope').exists()).toBe(true);
+ expect(findAddNewScopeRow().exists()).toBe(true);
});
describe('status toggle', () => {
describe('without filled text input', () => {
it('should add a new scope with the text value empty and the status', () => {
- wrapper.find(ToggleButton).vm.$emit('change', true);
+ findGlToggle().vm.$emit('change', true);
expect(wrapper.vm.formScopes).toHaveLength(1);
expect(wrapper.vm.formScopes[0].active).toEqual(true);
@@ -121,7 +126,7 @@ describe('feature flag form', () => {
it('should be disabled if the feature flag is not active', (done) => {
wrapper.setProps({ active: false });
wrapper.vm.$nextTick(() => {
- expect(wrapper.find(ToggleButton).props('disabledInput')).toBe(true);
+ expect(findGlToggle().props('disabled')).toBe(true);
done();
});
});
@@ -166,11 +171,11 @@ describe('feature flag form', () => {
describe('scopes', () => {
it('should be possible to remove a scope', () => {
- expect(wrapper.find('.js-feature-flag-delete').exists()).toEqual(true);
+ expect(wrapper.findByTestId('feature-flag-delete').exists()).toEqual(true);
});
it('renders empty row to add a new scope', () => {
- expect(wrapper.find('.js-add-new-scope').exists()).toEqual(true);
+ expect(findAddNewScopeRow().exists()).toEqual(true);
});
it('renders the user id checkbox', () => {
@@ -186,7 +191,7 @@ describe('feature flag form', () => {
describe('update scope', () => {
describe('on click on toggle', () => {
it('should update the scope', () => {
- wrapper.find(ToggleButton).vm.$emit('change', false);
+ findGlToggle().vm.$emit('change', false);
expect(wrapper.vm.formScopes[0].active).toBe(false);
});
@@ -195,7 +200,7 @@ describe('feature flag form', () => {
wrapper.setProps({ active: false });
wrapper.vm.$nextTick(() => {
- expect(wrapper.find(ToggleButton).props('disabledInput')).toBe(true);
+ expect(findGlToggle().props('disabled')).toBe(true);
done();
});
});
@@ -294,7 +299,7 @@ describe('feature flag form', () => {
const row = wrapper.findAll('.gl-responsive-table-row').at(2);
expect(row.find(EnvironmentsDropdown).vm.disabled).toBe(true);
- expect(row.find(ToggleButton).vm.disabledInput).toBe(true);
+ expect(row.find(GlToggle).props('disabled')).toBe(true);
expect(row.find('.js-delete-scope').exists()).toBe(false);
});
});
@@ -347,10 +352,10 @@ describe('feature flag form', () => {
return wrapper.vm.$nextTick();
})
.then(() => {
- wrapper.find('.js-add-new-scope').find(ToggleButton).vm.$emit('change', true);
+ findAddNewScopeRow().find(GlToggle).vm.$emit('change', true);
})
.then(() => {
- wrapper.find(ToggleButton).vm.$emit('change', true);
+ findGlToggle().vm.$emit('change', true);
return wrapper.vm.$nextTick();
})
diff --git a/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js b/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js
index ad58ceaf5f8..6342ac0bda7 100644
--- a/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js
+++ b/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js
@@ -1,6 +1,6 @@
+import { GlLoadingIcon, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import { GlLoadingIcon, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui';
import NewEnvironmentsDropdown from '~/feature_flags/components/new_environments_dropdown.vue';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
diff --git a/spec/frontend/feature_flags/components/new_feature_flag_spec.js b/spec/frontend/feature_flags/components/new_feature_flag_spec.js
index e317ac4b092..e209c14d8c7 100644
--- a/spec/frontend/feature_flags/components/new_feature_flag_spec.js
+++ b/spec/frontend/feature_flags/components/new_feature_flag_spec.js
@@ -1,11 +1,11 @@
+import { GlAlert } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import { GlAlert } from '@gitlab/ui';
import { TEST_HOST } from 'spec/test_constants';
import Form from '~/feature_flags/components/form.vue';
-import createStore from '~/feature_flags/store/new';
import NewFeatureFlag from '~/feature_flags/components/new_feature_flag.vue';
import { ROLLOUT_STRATEGY_ALL_USERS, DEFAULT_PERCENT_ROLLOUT } from '~/feature_flags/constants';
+import createStore from '~/feature_flags/store/new';
import { allUsersStrategy } from '../mock_data';
const userCalloutId = 'feature_flags_new_version';
@@ -41,6 +41,9 @@ describe('New feature flag form', () => {
});
};
+ const findWarningGlAlert = () =>
+ wrapper.findAll(GlAlert).filter((c) => c.props('variant') === 'warning');
+
beforeEach(() => {
factory();
});
@@ -53,8 +56,9 @@ describe('New feature flag form', () => {
it('should render the error', () => {
store.dispatch('receiveCreateFeatureFlagError', { message: ['The name is required'] });
return wrapper.vm.$nextTick(() => {
- expect(wrapper.find('.alert').exists()).toEqual(true);
- expect(wrapper.find('.alert').text()).toContain('The name is required');
+ const warningGlAlert = findWarningGlAlert();
+ expect(warningGlAlert.at(0).exists()).toBe(true);
+ expect(warningGlAlert.at(0).text()).toContain('The name is required');
});
});
});
@@ -81,10 +85,6 @@ describe('New feature flag form', () => {
expect(wrapper.find(Form).props('scopes')).toContainEqual(defaultScope);
});
- it('should not alert users that feature flags are changing soon', () => {
- expect(wrapper.find(GlAlert).exists()).toBe(false);
- });
-
it('has an all users strategy by default', () => {
const strategies = wrapper.find(Form).props('strategies');
diff --git a/spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js b/spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js
index 725f53d4409..02216370b79 100644
--- a/spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js
+++ b/spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js
@@ -1,5 +1,5 @@
-import { mount } from '@vue/test-utils';
import { GlFormInput, GlFormSelect } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import FlexibleRollout from '~/feature_flags/components/strategies/flexible_rollout.vue';
import ParameterFormGroup from '~/feature_flags/components/strategies/parameter_form_group.vue';
import { PERCENT_ROLLOUT_GROUP_ID } from '~/feature_flags/constants';
diff --git a/spec/frontend/feature_flags/components/strategies/gitlab_user_list_spec.js b/spec/frontend/feature_flags/components/strategies/gitlab_user_list_spec.js
index 1c85eadc678..6188672b23b 100644
--- a/spec/frontend/feature_flags/components/strategies/gitlab_user_list_spec.js
+++ b/spec/frontend/feature_flags/components/strategies/gitlab_user_list_spec.js
@@ -1,9 +1,9 @@
+import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlLoadingIcon } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlLoadingIcon } from '@gitlab/ui';
import Api from '~/api';
-import createStore from '~/feature_flags/store/new';
import GitlabUserList from '~/feature_flags/components/strategies/gitlab_user_list.vue';
+import createStore from '~/feature_flags/store/new';
import { userListStrategy, userList } from '../../mock_data';
jest.mock('~/api');
diff --git a/spec/frontend/feature_flags/components/strategies/parameter_form_group_spec.js b/spec/frontend/feature_flags/components/strategies/parameter_form_group_spec.js
index a0ffdb1fca0..33696064d55 100644
--- a/spec/frontend/feature_flags/components/strategies/parameter_form_group_spec.js
+++ b/spec/frontend/feature_flags/components/strategies/parameter_form_group_spec.js
@@ -1,5 +1,5 @@
-import { mount } from '@vue/test-utils';
import { GlFormGroup, GlFormInput } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import ParameterFormGroup from '~/feature_flags/components/strategies/parameter_form_group.vue';
describe('~/feature_flags/strategies/parameter_form_group.vue', () => {
diff --git a/spec/frontend/feature_flags/components/strategies/percent_rollout_spec.js b/spec/frontend/feature_flags/components/strategies/percent_rollout_spec.js
index 696b3b2e4c9..442f7faf161 100644
--- a/spec/frontend/feature_flags/components/strategies/percent_rollout_spec.js
+++ b/spec/frontend/feature_flags/components/strategies/percent_rollout_spec.js
@@ -1,7 +1,7 @@
-import { mount } from '@vue/test-utils';
import { GlFormInput } from '@gitlab/ui';
-import PercentRollout from '~/feature_flags/components/strategies/percent_rollout.vue';
+import { mount } from '@vue/test-utils';
import ParameterFormGroup from '~/feature_flags/components/strategies/parameter_form_group.vue';
+import PercentRollout from '~/feature_flags/components/strategies/percent_rollout.vue';
import { PERCENT_ROLLOUT_GROUP_ID } from '~/feature_flags/constants';
import { percentRolloutStrategy } from '../../mock_data';
diff --git a/spec/frontend/feature_flags/components/strategies/users_with_id_spec.js b/spec/frontend/feature_flags/components/strategies/users_with_id_spec.js
index 460df6ef2ec..745fbca00fe 100644
--- a/spec/frontend/feature_flags/components/strategies/users_with_id_spec.js
+++ b/spec/frontend/feature_flags/components/strategies/users_with_id_spec.js
@@ -1,5 +1,5 @@
-import { mount } from '@vue/test-utils';
import { GlFormTextarea } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import UsersWithId from '~/feature_flags/components/strategies/users_with_id.vue';
import { usersWithIdStrategy } from '../../mock_data';
diff --git a/spec/frontend/feature_flags/components/strategy_parameters_spec.js b/spec/frontend/feature_flags/components/strategy_parameters_spec.js
index 82811c05195..979ca255b08 100644
--- a/spec/frontend/feature_flags/components/strategy_parameters_spec.js
+++ b/spec/frontend/feature_flags/components/strategy_parameters_spec.js
@@ -1,16 +1,16 @@
import { shallowMount } from '@vue/test-utils';
import { last } from 'lodash';
+import Default from '~/feature_flags/components/strategies/default.vue';
+import GitlabUserList from '~/feature_flags/components/strategies/gitlab_user_list.vue';
+import PercentRollout from '~/feature_flags/components/strategies/percent_rollout.vue';
+import UsersWithId from '~/feature_flags/components/strategies/users_with_id.vue';
+import StrategyParameters from '~/feature_flags/components/strategy_parameters.vue';
import {
ROLLOUT_STRATEGY_ALL_USERS,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
ROLLOUT_STRATEGY_USER_ID,
ROLLOUT_STRATEGY_GITLAB_USER_LIST,
} from '~/feature_flags/constants';
-import Default from '~/feature_flags/components/strategies/default.vue';
-import GitlabUserList from '~/feature_flags/components/strategies/gitlab_user_list.vue';
-import PercentRollout from '~/feature_flags/components/strategies/percent_rollout.vue';
-import UsersWithId from '~/feature_flags/components/strategies/users_with_id.vue';
-import StrategyParameters from '~/feature_flags/components/strategy_parameters.vue';
import { allUsersStrategy } from '../mock_data';
const DEFAULT_PROPS = {
diff --git a/spec/frontend/feature_flags/components/strategy_spec.js b/spec/frontend/feature_flags/components/strategy_spec.js
index 67cf70c37e2..4fdf436bfc4 100644
--- a/spec/frontend/feature_flags/components/strategy_spec.js
+++ b/spec/frontend/feature_flags/components/strategy_spec.js
@@ -1,9 +1,11 @@
+import { GlAlert, GlFormSelect, GlLink, GlToken, GlButton } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
-import Vuex from 'vuex';
import { last } from 'lodash';
-import { GlAlert, GlFormSelect, GlLink, GlToken, GlButton } from '@gitlab/ui';
+import Vuex from 'vuex';
import Api from '~/api';
-import createStore from '~/feature_flags/store/new';
+import NewEnvironmentsDropdown from '~/feature_flags/components/new_environments_dropdown.vue';
+import Strategy from '~/feature_flags/components/strategy.vue';
+import StrategyParameters from '~/feature_flags/components/strategy_parameters.vue';
import {
PERCENT_ROLLOUT_GROUP_ID,
ROLLOUT_STRATEGY_ALL_USERS,
@@ -12,9 +14,7 @@ import {
ROLLOUT_STRATEGY_USER_ID,
ROLLOUT_STRATEGY_GITLAB_USER_LIST,
} from '~/feature_flags/constants';
-import Strategy from '~/feature_flags/components/strategy.vue';
-import NewEnvironmentsDropdown from '~/feature_flags/components/new_environments_dropdown.vue';
-import StrategyParameters from '~/feature_flags/components/strategy_parameters.vue';
+import createStore from '~/feature_flags/store/new';
import { userList } from '../mock_data';
diff --git a/spec/frontend/feature_flags/components/user_lists_table_spec.js b/spec/frontend/feature_flags/components/user_lists_table_spec.js
index 974f63ba934..1b04ecee146 100644
--- a/spec/frontend/feature_flags/components/user_lists_table_spec.js
+++ b/spec/frontend/feature_flags/components/user_lists_table_spec.js
@@ -1,6 +1,6 @@
+import { GlModal } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import * as timeago from 'timeago.js';
-import { GlModal } from '@gitlab/ui';
import UserListsTable from '~/feature_flags/components/user_lists_table.vue';
import { userList } from '../mock_data';
diff --git a/spec/frontend/feature_flags/store/edit/actions_spec.js b/spec/frontend/feature_flags/store/edit/actions_spec.js
index 20cec5daac0..afcac53468c 100644
--- a/spec/frontend/feature_flags/store/edit/actions_spec.js
+++ b/spec/frontend/feature_flags/store/edit/actions_spec.js
@@ -2,6 +2,11 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'spec/test_constants';
import {
+ NEW_VERSION_FLAG,
+ LEGACY_FLAG,
+ ROLLOUT_STRATEGY_ALL_USERS,
+} from '~/feature_flags/constants';
+import {
updateFeatureFlag,
requestUpdateFeatureFlag,
receiveUpdateFeatureFlagSuccess,
@@ -12,14 +17,9 @@ import {
receiveFeatureFlagError,
toggleActive,
} from '~/feature_flags/store/edit/actions';
+import * as types from '~/feature_flags/store/edit/mutation_types';
import state from '~/feature_flags/store/edit/state';
import { mapStrategiesToRails, mapFromScopesViewModel } from '~/feature_flags/store/helpers';
-import {
- NEW_VERSION_FLAG,
- LEGACY_FLAG,
- ROLLOUT_STRATEGY_ALL_USERS,
-} from '~/feature_flags/constants';
-import * as types from '~/feature_flags/store/edit/mutation_types';
import axios from '~/lib/utils/axios_utils';
jest.mock('~/lib/utils/url_utility');
diff --git a/spec/frontend/feature_flags/store/edit/mutations_spec.js b/spec/frontend/feature_flags/store/edit/mutations_spec.js
index 1d817fb8004..4b43f8b82df 100644
--- a/spec/frontend/feature_flags/store/edit/mutations_spec.js
+++ b/spec/frontend/feature_flags/store/edit/mutations_spec.js
@@ -1,6 +1,6 @@
-import state from '~/feature_flags/store/edit/state';
-import mutations from '~/feature_flags/store/edit/mutations';
import * as types from '~/feature_flags/store/edit/mutation_types';
+import mutations from '~/feature_flags/store/edit/mutations';
+import state from '~/feature_flags/store/edit/state';
describe('Feature flags Edit Module Mutations', () => {
let stateCopy;
diff --git a/spec/frontend/feature_flags/store/gitlab_user_lists/actions_spec.js b/spec/frontend/feature_flags/store/gitlab_user_lists/actions_spec.js
index aba578cca59..b4887d23e4b 100644
--- a/spec/frontend/feature_flags/store/gitlab_user_lists/actions_spec.js
+++ b/spec/frontend/feature_flags/store/gitlab_user_lists/actions_spec.js
@@ -1,8 +1,8 @@
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
-import createState from '~/feature_flags/store/gitlab_user_list/state';
import { fetchUserLists, setFilter } from '~/feature_flags/store/gitlab_user_list/actions';
import * as types from '~/feature_flags/store/gitlab_user_list/mutation_types';
+import createState from '~/feature_flags/store/gitlab_user_list/state';
import { userList } from '../../mock_data';
jest.mock('~/api');
diff --git a/spec/frontend/feature_flags/store/gitlab_user_lists/getters_spec.js b/spec/frontend/feature_flags/store/gitlab_user_lists/getters_spec.js
index e267cd59f50..1f02cbb44a1 100644
--- a/spec/frontend/feature_flags/store/gitlab_user_lists/getters_spec.js
+++ b/spec/frontend/feature_flags/store/gitlab_user_lists/getters_spec.js
@@ -4,8 +4,8 @@ import {
isLoading,
hasError,
} from '~/feature_flags/store/gitlab_user_list/getters';
-import statuses from '~/feature_flags/store/gitlab_user_list/status';
import createState from '~/feature_flags/store/gitlab_user_list/state';
+import statuses from '~/feature_flags/store/gitlab_user_list/status';
import { userList } from '../../mock_data';
describe('~/feature_flags/store/gitlab_user_list/getters', () => {
diff --git a/spec/frontend/feature_flags/store/gitlab_user_lists/mutations_spec.js b/spec/frontend/feature_flags/store/gitlab_user_lists/mutations_spec.js
index 88d4554a227..46233c43b07 100644
--- a/spec/frontend/feature_flags/store/gitlab_user_lists/mutations_spec.js
+++ b/spec/frontend/feature_flags/store/gitlab_user_lists/mutations_spec.js
@@ -1,7 +1,7 @@
-import statuses from '~/feature_flags/store/gitlab_user_list/status';
-import createState from '~/feature_flags/store/gitlab_user_list/state';
import * as types from '~/feature_flags/store/gitlab_user_list/mutation_types';
import mutations from '~/feature_flags/store/gitlab_user_list/mutations';
+import createState from '~/feature_flags/store/gitlab_user_list/state';
+import statuses from '~/feature_flags/store/gitlab_user_list/status';
import { userList } from '../../mock_data';
describe('~/feature_flags/store/gitlab_user_list/mutations', () => {
diff --git a/spec/frontend/feature_flags/store/helpers_spec.js b/spec/frontend/feature_flags/store/helpers_spec.js
index 301b1d09fcc..711e2a1286e 100644
--- a/spec/frontend/feature_flags/store/helpers_spec.js
+++ b/spec/frontend/feature_flags/store/helpers_spec.js
@@ -1,12 +1,5 @@
import { uniqueId } from 'lodash';
import {
- mapToScopesViewModel,
- mapFromScopesViewModel,
- createNewEnvironmentScope,
- mapStrategiesToViewModel,
- mapStrategiesToRails,
-} from '~/feature_flags/store/helpers';
-import {
ROLLOUT_STRATEGY_ALL_USERS,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
ROLLOUT_STRATEGY_USER_ID,
@@ -16,6 +9,13 @@ import {
LEGACY_FLAG,
NEW_VERSION_FLAG,
} from '~/feature_flags/constants';
+import {
+ mapToScopesViewModel,
+ mapFromScopesViewModel,
+ createNewEnvironmentScope,
+ mapStrategiesToViewModel,
+ mapStrategiesToRails,
+} from '~/feature_flags/store/helpers';
describe('feature flags helpers spec', () => {
describe('mapToScopesViewModel', () => {
diff --git a/spec/frontend/feature_flags/store/index/actions_spec.js b/spec/frontend/feature_flags/store/index/actions_spec.js
index 82e16958d33..a7ab2e92cb2 100644
--- a/spec/frontend/feature_flags/store/index/actions_spec.js
+++ b/spec/frontend/feature_flags/store/index/actions_spec.js
@@ -2,6 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'spec/test_constants';
import Api from '~/api';
+import { mapToScopesViewModel } from '~/feature_flags/store/helpers';
import {
requestFeatureFlags,
receiveFeatureFlagsSuccess,
@@ -24,9 +25,8 @@ import {
receiveDeleteUserListError,
clearAlert,
} from '~/feature_flags/store/index/actions';
-import { mapToScopesViewModel } from '~/feature_flags/store/helpers';
-import state from '~/feature_flags/store/index/state';
import * as types from '~/feature_flags/store/index/mutation_types';
+import state from '~/feature_flags/store/index/state';
import axios from '~/lib/utils/axios_utils';
import { getRequestData, rotateData, featureFlag, userList } from '../../mock_data';
diff --git a/spec/frontend/feature_flags/store/index/mutations_spec.js b/spec/frontend/feature_flags/store/index/mutations_spec.js
index eec56800a8d..08b5868d1b4 100644
--- a/spec/frontend/feature_flags/store/index/mutations_spec.js
+++ b/spec/frontend/feature_flags/store/index/mutations_spec.js
@@ -1,7 +1,7 @@
-import state from '~/feature_flags/store/index/state';
-import mutations from '~/feature_flags/store/index/mutations';
-import * as types from '~/feature_flags/store/index/mutation_types';
import { mapToScopesViewModel } from '~/feature_flags/store/helpers';
+import * as types from '~/feature_flags/store/index/mutation_types';
+import mutations from '~/feature_flags/store/index/mutations';
+import state from '~/feature_flags/store/index/state';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { getRequestData, rotateData, featureFlag, userList } from '../../mock_data';
diff --git a/spec/frontend/feature_flags/store/new/actions_spec.js b/spec/frontend/feature_flags/store/new/actions_spec.js
index 9168f11fdfb..00dfb982ded 100644
--- a/spec/frontend/feature_flags/store/new/actions_spec.js
+++ b/spec/frontend/feature_flags/store/new/actions_spec.js
@@ -2,20 +2,20 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'spec/test_constants';
import {
- createFeatureFlag,
- requestCreateFeatureFlag,
- receiveCreateFeatureFlagSuccess,
- receiveCreateFeatureFlagError,
-} from '~/feature_flags/store/new/actions';
-import state from '~/feature_flags/store/new/state';
-import * as types from '~/feature_flags/store/new/mutation_types';
-import {
ROLLOUT_STRATEGY_ALL_USERS,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
LEGACY_FLAG,
NEW_VERSION_FLAG,
} from '~/feature_flags/constants';
import { mapFromScopesViewModel, mapStrategiesToRails } from '~/feature_flags/store/helpers';
+import {
+ createFeatureFlag,
+ requestCreateFeatureFlag,
+ receiveCreateFeatureFlagSuccess,
+ receiveCreateFeatureFlagError,
+} from '~/feature_flags/store/new/actions';
+import * as types from '~/feature_flags/store/new/mutation_types';
+import state from '~/feature_flags/store/new/state';
import axios from '~/lib/utils/axios_utils';
jest.mock('~/lib/utils/url_utility');
diff --git a/spec/frontend/feature_flags/store/new/mutations_spec.js b/spec/frontend/feature_flags/store/new/mutations_spec.js
index e8609a6d116..c97e62247bb 100644
--- a/spec/frontend/feature_flags/store/new/mutations_spec.js
+++ b/spec/frontend/feature_flags/store/new/mutations_spec.js
@@ -1,6 +1,6 @@
-import state from '~/feature_flags/store/new/state';
-import mutations from '~/feature_flags/store/new/mutations';
import * as types from '~/feature_flags/store/new/mutation_types';
+import mutations from '~/feature_flags/store/new/mutations';
+import state from '~/feature_flags/store/new/state';
describe('Feature flags New Module Mutations', () => {
let stateCopy;
diff --git a/spec/frontend/feature_highlight/feature_highlight_helper_spec.js b/spec/frontend/feature_highlight/feature_highlight_helper_spec.js
index beae5041156..1b5bffc1f9b 100644
--- a/spec/frontend/feature_highlight/feature_highlight_helper_spec.js
+++ b/spec/frontend/feature_highlight/feature_highlight_helper_spec.js
@@ -1,62 +1,40 @@
-import $ from 'jquery';
+import MockAdapter from 'axios-mock-adapter';
+import { dismiss } from '~/feature_highlight/feature_highlight_helper';
+import { deprecatedCreateFlash as Flash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import { getSelector, dismiss, inserted } from '~/feature_highlight/feature_highlight_helper';
-import { togglePopover } from '~/shared/popover';
+import httpStatusCodes from '~/lib/utils/http_status';
-describe('feature highlight helper', () => {
- describe('getSelector', () => {
- it('returns js-feature-highlight selector', () => {
- const highlightId = 'highlightId';
-
- expect(getSelector(highlightId)).toEqual(
- `.js-feature-highlight[data-highlight=${highlightId}]`,
- );
- });
- });
+jest.mock('~/flash');
+describe('feature highlight helper', () => {
describe('dismiss', () => {
- const context = {
- hide: () => {},
- attr: () => '/-/callouts/dismiss',
- };
+ let mockAxios;
+ const endpoint = '/-/callouts/dismiss';
+ const highlightId = '123';
+ const { CREATED, INTERNAL_SERVER_ERROR } = httpStatusCodes;
beforeEach(() => {
- jest.spyOn(axios, 'post').mockResolvedValue();
- jest.spyOn(togglePopover, 'call').mockImplementation(() => {});
- jest.spyOn(context, 'hide').mockImplementation(() => {});
- dismiss.call(context);
+ mockAxios = new MockAdapter(axios);
});
- it('calls persistent dismissal endpoint', () => {
- expect(axios.post).toHaveBeenCalledWith(
- '/-/callouts/dismiss',
- expect.objectContaining({ feature_name: undefined }),
- );
+ afterEach(() => {
+ mockAxios.reset();
});
- it('calls hide popover', () => {
- expect(togglePopover.call).toHaveBeenCalledWith(context, false);
- });
+ it('calls persistent dismissal endpoint with highlightId', async () => {
+ mockAxios.onPost(endpoint, { feature_name: highlightId }).replyOnce(CREATED);
- it('calls hide', () => {
- expect(context.hide).toHaveBeenCalled();
+ await expect(dismiss(endpoint, highlightId)).resolves.toEqual(expect.anything());
});
- });
- describe('inserted', () => {
- it('registers click event callback', (done) => {
- const context = {
- getAttribute: () => 'popoverId',
- dataset: {
- highlight: 'some-feature',
- },
- };
-
- jest.spyOn($.fn, 'on').mockImplementation((event) => {
- expect(event).toEqual('click');
- done();
- });
- inserted.call(context);
+ it('triggers flash when dismiss request fails', async () => {
+ mockAxios.onPost(endpoint, { feature_name: highlightId }).replyOnce(INTERNAL_SERVER_ERROR);
+
+ await dismiss(endpoint, highlightId);
+
+ expect(Flash).toHaveBeenCalledWith(
+ 'An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again.',
+ );
});
});
});
diff --git a/spec/frontend/feature_highlight/feature_highlight_options_spec.js b/spec/frontend/feature_highlight/feature_highlight_options_spec.js
deleted file mode 100644
index f82f984cb7f..00000000000
--- a/spec/frontend/feature_highlight/feature_highlight_options_spec.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
-import domContentLoaded from '~/feature_highlight/feature_highlight_options';
-
-describe('feature highlight options', () => {
- describe('domContentLoaded', () => {
- it.each`
- breakPoint | shouldCall
- ${'xs'} | ${false}
- ${'sm'} | ${false}
- ${'md'} | ${false}
- ${'lg'} | ${false}
- ${'xl'} | ${true}
- `(
- 'when breakpoint is $breakPoint should call highlightFeatures is $shouldCall',
- ({ breakPoint, shouldCall }) => {
- jest.spyOn(bp, 'getBreakpointSize').mockReturnValue(breakPoint);
-
- expect(domContentLoaded()).toBe(shouldCall);
- },
- );
- });
-});
diff --git a/spec/frontend/feature_highlight/feature_highlight_popover_spec.js b/spec/frontend/feature_highlight/feature_highlight_popover_spec.js
new file mode 100644
index 00000000000..1d558366ce8
--- /dev/null
+++ b/spec/frontend/feature_highlight/feature_highlight_popover_spec.js
@@ -0,0 +1,80 @@
+import { GlPopover, GlLink, GlButton } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { POPOVER_TARGET_ID } from '~/feature_highlight/constants';
+import { dismiss } from '~/feature_highlight/feature_highlight_helper';
+import FeatureHighlightPopover from '~/feature_highlight/feature_highlight_popover.vue';
+
+jest.mock('~/feature_highlight/feature_highlight_helper');
+
+describe('feature_highlight/feature_highlight_popover', () => {
+ let wrapper;
+ const props = {
+ autoDevopsHelpPath: '/help/autodevops',
+ highlightId: '123',
+ dismissEndpoint: '/api/dismiss',
+ };
+
+ const buildWrapper = (propsData = props) => {
+ wrapper = mount(FeatureHighlightPopover, {
+ propsData,
+ });
+ };
+ const findPopoverTarget = () => wrapper.find(`#${POPOVER_TARGET_ID}`);
+ const findPopover = () => wrapper.findComponent(GlPopover);
+ const findAutoDevopsHelpLink = () => wrapper.findComponent(GlLink);
+ const findDismissButton = () => wrapper.findComponent(GlButton);
+
+ beforeEach(() => {
+ buildWrapper();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('renders popover target', () => {
+ expect(findPopoverTarget().exists()).toBe(true);
+ });
+
+ it('renders popover', () => {
+ expect(findPopover().props()).toMatchObject({
+ target: POPOVER_TARGET_ID,
+ cssClasses: ['feature-highlight-popover'],
+ triggers: 'hover',
+ container: 'body',
+ placement: 'right',
+ boundary: 'viewport',
+ });
+ });
+
+ it('renders link that points to the autodevops help page', () => {
+ expect(findAutoDevopsHelpLink().attributes().href).toBe(props.autoDevopsHelpPath);
+ expect(findAutoDevopsHelpLink().text()).toBe('Auto DevOps');
+ });
+
+ it('renders dismiss button', () => {
+ expect(findDismissButton().props()).toMatchObject({
+ size: 'small',
+ icon: 'thumb-up',
+ variant: 'confirm',
+ });
+ });
+
+ it('dismisses popover when dismiss button is clicked', async () => {
+ await findDismissButton().trigger('click');
+
+ expect(findPopover().emitted('close')).toHaveLength(1);
+ expect(dismiss).toHaveBeenCalledWith(props.dismissEndpoint, props.highlightId);
+ });
+
+ describe('when popover is dismissed and hidden', () => {
+ it('hides the popover target', async () => {
+ await findDismissButton().trigger('click');
+ findPopover().vm.$emit('hidden');
+ await wrapper.vm.$nextTick();
+
+ expect(findPopoverTarget().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/feature_highlight/feature_highlight_spec.js b/spec/frontend/feature_highlight/feature_highlight_spec.js
deleted file mode 100644
index 79c4050c8c4..00000000000
--- a/spec/frontend/feature_highlight/feature_highlight_spec.js
+++ /dev/null
@@ -1,120 +0,0 @@
-import $ from 'jquery';
-import MockAdapter from 'axios-mock-adapter';
-import * as featureHighlight from '~/feature_highlight/feature_highlight';
-import * as popover from '~/shared/popover';
-import axios from '~/lib/utils/axios_utils';
-
-jest.mock('~/shared/popover');
-
-describe('feature highlight', () => {
- beforeEach(() => {
- setFixtures(`
- <div>
- <div class="js-feature-highlight" data-highlight="test" data-highlight-priority="10" data-dismiss-endpoint="/test" disabled>
- Trigger
- </div>
- </div>
- <div class="feature-highlight-popover-content">
- Content
- <div class="dismiss-feature-highlight">
- Dismiss
- </div>
- </div>
- `);
- });
-
- describe('setupFeatureHighlightPopover', () => {
- let mock;
- const selector = '.js-feature-highlight[data-highlight=test]';
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- mock.onGet('/test').reply(200);
- jest.spyOn(window, 'addEventListener').mockImplementation(() => {});
- featureHighlight.setupFeatureHighlightPopover('test', 0);
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- it('setup popover content', () => {
- const $popoverContent = $('.feature-highlight-popover-content');
- const outerHTML = $popoverContent.prop('outerHTML');
-
- expect($(selector).data('content')).toEqual(outerHTML);
- });
-
- it('setup mouseenter', () => {
- $(selector).trigger('mouseenter');
-
- expect(popover.mouseenter).toHaveBeenCalledWith(expect.any(Object));
- });
-
- it('setup debounced mouseleave', () => {
- $(selector).trigger('mouseleave');
-
- expect(popover.debouncedMouseleave).toHaveBeenCalled();
- });
-
- it('setup show.bs.popover', () => {
- $(selector).trigger('show.bs.popover');
-
- expect(window.addEventListener).toHaveBeenCalledWith('scroll', expect.any(Function), {
- once: true,
- });
- });
-
- it('removes disabled attribute', () => {
- expect($('.js-feature-highlight').is(':disabled')).toEqual(false);
- });
- });
-
- describe('findHighestPriorityFeature', () => {
- beforeEach(() => {
- setFixtures(`
- <div class="js-feature-highlight" data-highlight="test" data-highlight-priority="10" disabled></div>
- <div class="js-feature-highlight" data-highlight="test-high-priority" data-highlight-priority="20" disabled></div>
- <div class="js-feature-highlight" data-highlight="test-low-priority" data-highlight-priority="0" disabled></div>
- `);
- });
-
- it('should pick the highest priority feature highlight', () => {
- setFixtures(`
- <div class="js-feature-highlight" data-highlight="test" data-highlight-priority="10" disabled></div>
- <div class="js-feature-highlight" data-highlight="test-high-priority" data-highlight-priority="20" disabled></div>
- <div class="js-feature-highlight" data-highlight="test-low-priority" data-highlight-priority="0" disabled></div>
- `);
-
- expect($('.js-feature-highlight').length).toBeGreaterThan(1);
- expect(featureHighlight.findHighestPriorityFeature()).toEqual('test-high-priority');
- });
-
- it('should work when no priority is set', () => {
- setFixtures(`
- <div class="js-feature-highlight" data-highlight="test" disabled></div>
- `);
-
- expect(featureHighlight.findHighestPriorityFeature()).toEqual('test');
- });
-
- it('should pick the highest priority feature highlight when some have no priority set', () => {
- setFixtures(`
- <div class="js-feature-highlight" data-highlight="test-no-priority1" disabled></div>
- <div class="js-feature-highlight" data-highlight="test" data-highlight-priority="10" disabled></div>
- <div class="js-feature-highlight" data-highlight="test-no-priority2" disabled></div>
- <div class="js-feature-highlight" data-highlight="test-high-priority" data-highlight-priority="20" disabled></div>
- <div class="js-feature-highlight" data-highlight="test-low-priority" data-highlight-priority="0" disabled></div>
- `);
-
- expect($('.js-feature-highlight').length).toBeGreaterThan(1);
- expect(featureHighlight.findHighestPriorityFeature()).toEqual('test-high-priority');
- });
- });
-
- describe('highlightFeatures', () => {
- it('calls setupFeatureHighlightPopover', () => {
- expect(featureHighlight.highlightFeatures()).toEqual('test');
- });
- });
-});
diff --git a/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js b/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js
index f1cff02261d..897ad5ee2bf 100644
--- a/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js
+++ b/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import eventHub from '~/filtered_search/event_hub';
import RecentSearchesDropdownContent from '~/filtered_search/components/recent_searches_dropdown_content.vue';
+import eventHub from '~/filtered_search/event_hub';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
describe('Recent Searches Dropdown Content', () => {
diff --git a/spec/frontend/filtered_search/dropdown_user_spec.js b/spec/frontend/filtered_search/dropdown_user_spec.js
index c1c09ea5d3e..0e2d2ee6c09 100644
--- a/spec/frontend/filtered_search/dropdown_user_spec.js
+++ b/spec/frontend/filtered_search/dropdown_user_spec.js
@@ -1,4 +1,7 @@
import DropdownUtils from '~/filtered_search/dropdown_utils';
+// TODO: Moving this line up throws an error about `FilteredSearchDropdown`
+// being undefined in test. See gitlab-org/gitlab#321476 for more info.
+// eslint-disable-next-line import/order
import DropdownUser from '~/filtered_search/dropdown_user';
import FilteredSearchTokenizer from '~/filtered_search/filtered_search_tokenizer';
import IssuableFilteredTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
diff --git a/spec/frontend/filtered_search/filtered_search_manager_spec.js b/spec/frontend/filtered_search/filtered_search_manager_spec.js
index def0aa14349..465e1ee1ef1 100644
--- a/spec/frontend/filtered_search/filtered_search_manager_spec.js
+++ b/spec/frontend/filtered_search/filtered_search_manager_spec.js
@@ -1,16 +1,16 @@
import FilteredSearchManager from 'ee_else_ce/filtered_search/filtered_search_manager';
import FilteredSearchSpecHelper from 'helpers/filtered_search_spec_helper';
-import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
-import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error';
-import RecentSearchesRoot from '~/filtered_search/recent_searches_root';
-import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import DropdownUtils from '~/filtered_search/dropdown_utils';
-import FilteredSearchVisualTokens from '~/filtered_search/filtered_search_visual_tokens';
import FilteredSearchDropdownManager from '~/filtered_search/filtered_search_dropdown_manager';
+import FilteredSearchVisualTokens from '~/filtered_search/filtered_search_visual_tokens';
+import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
+import RecentSearchesRoot from '~/filtered_search/recent_searches_root';
+import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
+import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error';
+import * as commonUtils from '~/lib/utils/common_utils';
import { BACKSPACE_KEY_CODE, DELETE_KEY_CODE } from '~/lib/utils/keycodes';
import { visitUrl } from '~/lib/utils/url_utility';
-import * as commonUtils from '~/lib/utils/common_utils';
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
diff --git a/spec/frontend/filtered_search/filtered_search_tokenizer_spec.js b/spec/frontend/filtered_search/filtered_search_tokenizer_spec.js
index dec03e5ab93..b6a95eb55c7 100644
--- a/spec/frontend/filtered_search/filtered_search_tokenizer_spec.js
+++ b/spec/frontend/filtered_search/filtered_search_tokenizer_spec.js
@@ -1,5 +1,5 @@
-import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import FilteredSearchTokenizer from '~/filtered_search/filtered_search_tokenizer';
+import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
describe('Filtered Search Tokenizer', () => {
const allowedKeys = IssuableFilteredSearchTokenKeys.getKeys();
diff --git a/spec/frontend/filtered_search/recent_searches_root_spec.js b/spec/frontend/filtered_search/recent_searches_root_spec.js
index 6bb9e68d591..fa3267c98a1 100644
--- a/spec/frontend/filtered_search/recent_searches_root_spec.js
+++ b/spec/frontend/filtered_search/recent_searches_root_spec.js
@@ -1,32 +1,51 @@
-import Vue from 'vue';
+import { setHTMLFixture } from 'helpers/fixtures';
import RecentSearchesRoot from '~/filtered_search/recent_searches_root';
-jest.mock('vue');
+const containerId = 'test-container';
+const dropdownElementId = 'test-dropdown-element';
describe('RecentSearchesRoot', () => {
describe('render', () => {
- let recentSearchesRoot;
- let data;
- let template;
+ let recentSearchesRootMockInstance;
+ let vm;
+ let containerEl;
beforeEach(() => {
- recentSearchesRoot = {
+ setHTMLFixture(`
+ <div id="${containerId}">
+ <div id="${dropdownElementId}"></div>
+ </div>
+ `);
+
+ containerEl = document.getElementById(containerId);
+
+ recentSearchesRootMockInstance = {
store: {
- state: 'state',
+ state: {
+ recentSearches: ['foo', 'bar', 'qux'],
+ isLocalStorageAvailable: true,
+ allowedKeys: ['test'],
+ },
},
+ wrapperElement: document.getElementById(dropdownElementId),
};
- Vue.mockImplementation((options) => {
- ({ data, template } = options);
- });
+ RecentSearchesRoot.prototype.render.call(recentSearchesRootMockInstance);
+ vm = recentSearchesRootMockInstance.vm;
- RecentSearchesRoot.prototype.render.call(recentSearchesRoot);
+ return vm.$nextTick();
});
- it('should instantiate Vue', () => {
- expect(Vue).toHaveBeenCalled();
- expect(data()).toBe(recentSearchesRoot.store.state);
- expect(template).toContain(':is-local-storage-available="isLocalStorageAvailable"');
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render the recent searches', () => {
+ const { recentSearches } = recentSearchesRootMockInstance.store.state;
+
+ recentSearches.forEach((recentSearch) => {
+ expect(containerEl.textContent).toContain(recentSearch);
+ });
});
});
});
diff --git a/spec/frontend/filtered_search/visual_token_value_spec.js b/spec/frontend/filtered_search/visual_token_value_spec.js
index 2a76c4a27df..a2082271efe 100644
--- a/spec/frontend/filtered_search/visual_token_value_spec.js
+++ b/spec/frontend/filtered_search/visual_token_value_spec.js
@@ -1,10 +1,10 @@
import { escape } from 'lodash';
-import { TEST_HOST } from 'helpers/test_constants';
import FilteredSearchSpecHelper from 'helpers/filtered_search_spec_helper';
+import { TEST_HOST } from 'helpers/test_constants';
+import DropdownUtils from '~/filtered_search//dropdown_utils';
import VisualTokenValue from '~/filtered_search/visual_token_value';
import AjaxCache from '~/lib/utils/ajax_cache';
import UsersCache from '~/lib/utils/users_cache';
-import DropdownUtils from '~/filtered_search//dropdown_utils';
describe('Filtered Search Visual Tokens', () => {
const findElements = (tokenElement) => {
diff --git a/spec/frontend/fixtures/api_merge_requests.rb b/spec/frontend/fixtures/api_merge_requests.rb
index f3280e216ff..7117c9a1c7a 100644
--- a/spec/frontend/fixtures/api_merge_requests.rb
+++ b/spec/frontend/fixtures/api_merge_requests.rb
@@ -6,9 +6,10 @@ RSpec.describe API::MergeRequests, '(JavaScript fixtures)', type: :request do
include ApiHelpers
include JavaScriptFixturesHelpers
- let(:admin) { create(:admin, name: 'root') }
- let(:namespace) { create(:namespace, name: 'gitlab-test' )}
- let(:project) { create(:project, :repository, namespace: namespace, path: 'lorem-ipsum') }
+ let_it_be(:admin) { create(:admin, name: 'root') }
+ let_it_be(:namespace) { create(:namespace, name: 'gitlab-test' )}
+ let_it_be(:project) { create(:project, :repository, namespace: namespace, path: 'lorem-ipsum') }
+ let_it_be(:mr) { create(:merge_request, source_project: project) }
before(:all) do
clean_frontend_fixtures('api/merge_requests')
@@ -21,4 +22,16 @@ RSpec.describe API::MergeRequests, '(JavaScript fixtures)', type: :request do
expect(response).to be_successful
end
+
+ it 'api/merge_requests/versions.json' do
+ get api("/projects/#{project.id}/merge_requests/#{mr.iid}/versions", admin)
+
+ expect(response).to be_successful
+ end
+
+ it 'api/merge_requests/changes.json' do
+ get api("/projects/#{project.id}/merge_requests/#{mr.iid}/changes", admin)
+
+ expect(response).to be_successful
+ end
end
diff --git a/spec/frontend/fixtures/merge_requests.rb b/spec/frontend/fixtures/merge_requests.rb
index acce3891ada..418912638f9 100644
--- a/spec/frontend/fixtures/merge_requests.rb
+++ b/spec/frontend/fixtures/merge_requests.rb
@@ -22,7 +22,6 @@ RSpec.describe Projects::MergeRequestsController, '(JavaScript fixtures)', type:
let(:merge_request) do
create(
:merge_request,
- :with_diffs,
source_project: project,
target_project: project,
description: description
diff --git a/spec/frontend/fixtures/merge_requests_diffs.rb b/spec/frontend/fixtures/merge_requests_diffs.rb
index 6e07ef679f5..5ad4176f7b8 100644
--- a/spec/frontend/fixtures/merge_requests_diffs.rb
+++ b/spec/frontend/fixtures/merge_requests_diffs.rb
@@ -8,7 +8,7 @@ RSpec.describe Projects::MergeRequests::DiffsController, '(JavaScript fixtures)'
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project, :repository, namespace: namespace, path: 'merge-requests-project') }
let(:user) { project.owner }
- let(:merge_request) { create(:merge_request, :with_diffs, source_project: project, target_project: project, description: '- [ ] Task List Item') }
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project, description: '- [ ] Task List Item') }
let(:path) { "files/ruby/popen.rb" }
let(:position) do
build(:text_diff_position, :added,
diff --git a/spec/frontend/fixtures/pipelines.rb b/spec/frontend/fixtures/pipelines.rb
index 4270e38afcb..b4b7f0e332f 100644
--- a/spec/frontend/fixtures/pipelines.rb
+++ b/spec/frontend/fixtures/pipelines.rb
@@ -12,7 +12,7 @@ RSpec.describe Projects::PipelinesController, '(JavaScript fixtures)', type: :co
let!(:user) { create(:user, developer_projects: [project], email: commit.author_email) }
let!(:pipeline) { create(:ci_pipeline, project: project, sha: commit.id, user: user) }
let!(:pipeline_without_author) { create(:ci_pipeline, project: project, sha: commit_without_author.id) }
- let!(:pipeline_without_commit) { create(:ci_pipeline, project: project, sha: '0000') }
+ let!(:pipeline_without_commit) { create(:ci_pipeline, status: :success, project: project, sha: '0000') }
render_views
diff --git a/spec/frontend/frequent_items/components/app_spec.js b/spec/frontend/frequent_items/components/app_spec.js
index b74e4ac45cf..80059c4c87f 100644
--- a/spec/frontend/frequent_items/components/app_spec.js
+++ b/spec/frontend/frequent_items/components/app_spec.js
@@ -1,15 +1,16 @@
import MockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
-import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
+import { useRealDate } from 'helpers/fake_date';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
+import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import axios from '~/lib/utils/axios_utils';
import appComponent from '~/frequent_items/components/app.vue';
-import eventHub from '~/frequent_items/event_hub';
import { FREQUENT_ITEMS, HOUR_IN_MS } from '~/frequent_items/constants';
+import eventHub from '~/frequent_items/event_hub';
+import { createStore } from '~/frequent_items/store';
import { getTopFrequentItems } from '~/frequent_items/utils';
+import axios from '~/lib/utils/axios_utils';
import { currentSession, mockFrequentProjects, mockSearchedProjects } from '../mock_data';
-import { createStore } from '~/frequent_items/store';
useLocalStorageSpy();
@@ -93,23 +94,27 @@ describe('Frequent Items App Component', () => {
expect(projects.length).toBe(1);
});
- it('should increase frequency of report if it was logged multiple times over the course of an hour', () => {
- let projects;
- const newTimestamp = Date.now() + HOUR_IN_MS + 1;
+ describe('with real date', () => {
+ useRealDate();
- vm.logItemAccess(session.storageKey, session.project);
- projects = JSON.parse(storage[session.storageKey]);
+ it('should increase frequency of report if it was logged multiple times over the course of an hour', () => {
+ let projects;
+ const newTimestamp = Date.now() + HOUR_IN_MS + 1;
- expect(projects[0].frequency).toBe(1);
+ vm.logItemAccess(session.storageKey, session.project);
+ projects = JSON.parse(storage[session.storageKey]);
- vm.logItemAccess(session.storageKey, {
- ...session.project,
- lastAccessedOn: newTimestamp,
- });
- projects = JSON.parse(storage[session.storageKey]);
+ expect(projects[0].frequency).toBe(1);
+
+ vm.logItemAccess(session.storageKey, {
+ ...session.project,
+ lastAccessedOn: newTimestamp,
+ });
+ projects = JSON.parse(storage[session.storageKey]);
- expect(projects[0].frequency).toBe(2);
- expect(projects[0].lastAccessedOn).not.toBe(session.project.lastAccessedOn);
+ expect(projects[0].frequency).toBe(2);
+ expect(projects[0].lastAccessedOn).not.toBe(session.project.lastAccessedOn);
+ });
});
it('should always update project metadata', () => {
diff --git a/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js b/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js
index 19095c4474e..66fb346cb38 100644
--- a/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js
+++ b/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { trimText } from 'helpers/text_helper';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import frequentItemsListItemComponent from '~/frequent_items/components/frequent_items_list_item.vue';
import { createStore } from '~/frequent_items/store';
import { mockProject } from '../mock_data';
diff --git a/spec/frontend/frequent_items/components/frequent_items_list_spec.js b/spec/frontend/frequent_items/components/frequent_items_list_spec.js
index 96f73ab1468..bd0711005b3 100644
--- a/spec/frontend/frequent_items/components/frequent_items_list_spec.js
+++ b/spec/frontend/frequent_items/components/frequent_items_list_spec.js
@@ -1,7 +1,7 @@
import { mount } from '@vue/test-utils';
-import { createStore } from '~/frequent_items/store';
import frequentItemsListComponent from '~/frequent_items/components/frequent_items_list.vue';
import frequentItemsListItemComponent from '~/frequent_items/components/frequent_items_list_item.vue';
+import { createStore } from '~/frequent_items/store';
import { mockFrequentProjects } from '../mock_data';
describe('FrequentItemsListComponent', () => {
diff --git a/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js b/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js
index cdd8b127676..0280fdb0ca2 100644
--- a/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js
+++ b/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js
@@ -1,8 +1,8 @@
+import { GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import searchComponent from '~/frequent_items/components/frequent_items_search_input.vue';
import { createStore } from '~/frequent_items/store';
-import eventHub from '~/frequent_items/event_hub';
describe('FrequentItemsSearchInputComponent', () => {
let wrapper;
@@ -16,6 +16,8 @@ describe('FrequentItemsSearchInputComponent', () => {
propsData: { namespace },
});
+ const findSearchBoxByType = () => wrapper.find(GlSearchBoxByType);
+
beforeEach(() => {
store = createStore({ dropdownType: 'project' });
jest.spyOn(store, 'dispatch').mockImplementation(() => {});
@@ -33,59 +35,13 @@ describe('FrequentItemsSearchInputComponent', () => {
vm.$destroy();
});
- describe('methods', () => {
- describe('setFocus', () => {
- it('should set focus to search input', () => {
- jest.spyOn(vm.$refs.search, 'focus').mockImplementation(() => {});
-
- vm.setFocus();
-
- expect(vm.$refs.search.focus).toHaveBeenCalled();
- });
- });
- });
-
- describe('mounted', () => {
- it('should listen `dropdownOpen` event', (done) => {
- jest.spyOn(eventHub, '$on').mockImplementation(() => {});
- const vmX = createComponent().vm;
-
- vmX.$nextTick(() => {
- expect(eventHub.$on).toHaveBeenCalledWith(
- `${vmX.namespace}-dropdownOpen`,
- expect.any(Function),
- );
- done();
- });
- });
- });
-
- describe('beforeDestroy', () => {
- it('should unbind event listeners on eventHub', (done) => {
- const vmX = createComponent().vm;
- jest.spyOn(eventHub, '$off').mockImplementation(() => {});
-
- vmX.$mount();
- vmX.$destroy();
-
- vmX.$nextTick(() => {
- expect(eventHub.$off).toHaveBeenCalledWith(
- `${vmX.namespace}-dropdownOpen`,
- expect.any(Function),
- );
- done();
- });
- });
- });
-
describe('template', () => {
it('should render component element', () => {
expect(wrapper.classes()).toContain('search-input-container');
- expect(wrapper.find('input.form-control').exists()).toBe(true);
- expect(wrapper.find('.search-icon').exists()).toBe(true);
- expect(wrapper.find('input.form-control').attributes('placeholder')).toBe(
- 'Search your projects',
- );
+ expect(findSearchBoxByType().exists()).toBe(true);
+ expect(findSearchBoxByType().attributes()).toMatchObject({
+ placeholder: 'Search your projects',
+ });
});
});
@@ -96,9 +52,7 @@ describe('FrequentItemsSearchInputComponent', () => {
const value = 'my project';
- const input = wrapper.find('input');
- input.setValue(value);
- input.trigger('input');
+ findSearchBoxByType().vm.$emit('input', value);
await wrapper.vm.$nextTick();
diff --git a/spec/frontend/frequent_items/store/actions_spec.js b/spec/frontend/frequent_items/store/actions_spec.js
index 351fde25f49..dacfc7ce707 100644
--- a/spec/frontend/frequent_items/store/actions_spec.js
+++ b/spec/frontend/frequent_items/store/actions_spec.js
@@ -1,10 +1,10 @@
-import testAction from 'helpers/vuex_action_helper';
import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import AccessorUtilities from '~/lib/utils/accessor';
+import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/frequent_items/store/actions';
import * as types from '~/frequent_items/store/mutation_types';
import state from '~/frequent_items/store/state';
+import AccessorUtilities from '~/lib/utils/accessor';
+import axios from '~/lib/utils/axios_utils';
import {
mockNamespace,
mockStorageKey,
diff --git a/spec/frontend/frequent_items/store/getters_spec.js b/spec/frontend/frequent_items/store/getters_spec.js
index 1cd12eb6832..97732cd95fc 100644
--- a/spec/frontend/frequent_items/store/getters_spec.js
+++ b/spec/frontend/frequent_items/store/getters_spec.js
@@ -1,5 +1,5 @@
-import state from '~/frequent_items/store/state';
import * as getters from '~/frequent_items/store/getters';
+import state from '~/frequent_items/store/state';
describe('Frequent Items Dropdown Store Getters', () => {
let mockedState;
diff --git a/spec/frontend/frequent_items/store/mutations_spec.js b/spec/frontend/frequent_items/store/mutations_spec.js
index d36964b2600..e593c9fae58 100644
--- a/spec/frontend/frequent_items/store/mutations_spec.js
+++ b/spec/frontend/frequent_items/store/mutations_spec.js
@@ -1,6 +1,6 @@
-import state from '~/frequent_items/store/state';
-import mutations from '~/frequent_items/store/mutations';
import * as types from '~/frequent_items/store/mutation_types';
+import mutations from '~/frequent_items/store/mutations';
+import state from '~/frequent_items/store/state';
import {
mockNamespace,
mockStorageKey,
diff --git a/spec/frontend/frequent_items/utils_spec.js b/spec/frontend/frequent_items/utils_spec.js
index 181dd9268dc..a7ab18b0d10 100644
--- a/spec/frontend/frequent_items/utils_spec.js
+++ b/spec/frontend/frequent_items/utils_spec.js
@@ -1,11 +1,11 @@
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
+import { HOUR_IN_MS, FREQUENT_ITEMS } from '~/frequent_items/constants';
import {
isMobile,
getTopFrequentItems,
updateExistingFrequentItem,
sanitizeItem,
} from '~/frequent_items/utils';
-import { HOUR_IN_MS, FREQUENT_ITEMS } from '~/frequent_items/constants';
import { mockProject, unsortedFrequentItems, sortedFrequentItems } from './mock_data';
describe('Frequent Items utils spec', () => {
diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js
index c2ff66f6afc..08368e1f2ca 100644
--- a/spec/frontend/gfm_auto_complete_spec.js
+++ b/spec/frontend/gfm_auto_complete_spec.js
@@ -1,16 +1,12 @@
/* eslint no-param-reassign: "off" */
-
+import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
-import { emojiFixtureMap, initEmojiMock, describeEmojiFields } from 'helpers/emoji';
-import '~/lib/utils/jquery_at_who';
import GfmAutoComplete, { membersBeforeSave } from 'ee_else_ce/gfm_auto_complete';
-
-import { TEST_HOST } from 'helpers/test_constants';
+import { initEmojiMock } from 'helpers/emoji';
+import '~/lib/utils/jquery_at_who';
import { getJSONFixture } from 'helpers/fixtures';
-
+import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
-
-import MockAdapter from 'axios-mock-adapter';
import AjaxCache from '~/lib/utils/ajax_cache';
import axios from '~/lib/utils/axios_utils';
@@ -493,7 +489,7 @@ describe('GfmAutoComplete', () => {
username: 'my-group',
avatarTag: '<div class="avatar rect-avatar center avatar-inline s26">M</div>',
title: 'My Group (2)',
- search: 'my-group My Group',
+ search: 'MyGroup my-group',
icon: '',
},
]);
@@ -506,7 +502,7 @@ describe('GfmAutoComplete', () => {
avatarTag:
'<img src="./group.jpg" alt="my-group" class="avatar rect-avatar avatar-inline center s26"/>',
title: 'My Group (2)',
- search: 'my-group My Group',
+ search: 'MyGroup my-group',
icon: '',
},
]);
@@ -519,7 +515,7 @@ describe('GfmAutoComplete', () => {
avatarTag:
'<img src="./group.jpg" alt="my-group" class="avatar rect-avatar avatar-inline center s26"/>',
title: 'My Group',
- search: 'my-group My Group',
+ search: 'MyGroup my-group',
icon:
'<svg class="s16 vertical-align-middle gl-ml-2"><use xlink:href="undefined#notifications-off" /></svg>',
},
@@ -537,7 +533,7 @@ describe('GfmAutoComplete', () => {
avatarTag:
'<img src="./users.jpg" alt="my-user" class="avatar avatar-inline center s26"/>',
title: 'My User',
- search: 'my-user My User',
+ search: 'MyUser my-user',
icon: '',
},
]);
@@ -717,16 +713,20 @@ describe('GfmAutoComplete', () => {
});
describe('emoji', () => {
- const { atom, heart, star } = emojiFixtureMap;
- const assertInserted = ({ input, subject, emoji }) =>
- expect(subject).toBe(`:${emoji?.name || input}:`);
- const assertTemplated = ({ input, subject, emoji, field }) =>
- expect(subject.replace(/\s+/g, ' ')).toBe(
- `<li>${field || input} <gl-emoji data-name="${emoji?.name || input}"></gl-emoji> </li>`,
- );
-
let mock;
+ const mockItem = {
+ 'atwho-at': ':',
+ emoji: {
+ c: 'symbols',
+ d: 'negative squared ab',
+ e: '🆎',
+ name: 'ab',
+ u: '6.0',
+ },
+ fieldValue: 'ab',
+ };
+
beforeEach(async () => {
mock = await initEmojiMock();
@@ -738,90 +738,22 @@ describe('GfmAutoComplete', () => {
mock.restore();
});
- describe.each`
- name | inputFormat | assert
- ${'insertTemplateFunction'} | ${(name) => ({ name })} | ${assertInserted}
- ${'templateFunction'} | ${(name) => name} | ${assertTemplated}
- `('Emoji.$name', ({ name, inputFormat, assert }) => {
- const execute = (accessor, input, emoji) =>
- assert({
- input,
- emoji,
- field: accessor && accessor(emoji),
- subject: GfmAutoComplete.Emoji[name](inputFormat(input)),
- });
-
- describeEmojiFields('for $field', ({ accessor }) => {
- it('should work with lowercase', () => {
- execute(accessor, accessor(atom), atom);
- });
-
- it('should work with uppercase', () => {
- execute(accessor, accessor(atom).toUpperCase(), atom);
- });
-
- it('should work with partial value', () => {
- execute(accessor, accessor(atom).slice(1), atom);
- });
- });
-
- it('should work with unicode value', () => {
- execute(null, atom.moji, atom);
- });
+ describe('Emoji.templateFunction', () => {
+ it('should return a correct template', () => {
+ const actual = GfmAutoComplete.Emoji.templateFunction(mockItem);
+ const glEmojiTag = `<gl-emoji data-name="${mockItem.emoji.name}"></gl-emoji>`;
+ const expected = `<li>${mockItem.fieldValue} ${glEmojiTag}</li>`;
- it('should pass through unknown value', () => {
- execute(null, 'foo bar baz');
+ expect(actual).toBe(expected);
});
});
- const expectEmojiOrder = (first, second) => {
- const keys = Object.keys(emojiFixtureMap);
- const firstIndex = keys.indexOf(first);
- const secondIndex = keys.indexOf(second);
- expect(firstIndex).toBeGreaterThanOrEqual(0);
- expect(secondIndex).toBeGreaterThanOrEqual(0);
- expect(firstIndex).toBeLessThan(secondIndex);
- };
-
describe('Emoji.insertTemplateFunction', () => {
- it('should map ":heart" to :heart: [regression]', () => {
- // the bug mapped heart to black_heart because the latter sorted first
- expectEmojiOrder('black_heart', 'heart');
-
- const item = GfmAutoComplete.Emoji.insertTemplateFunction({ name: 'heart' });
- expect(item).toEqual(`:${heart.name}:`);
- });
-
- it('should map ":star" to :star: [regression]', () => {
- // the bug mapped star to custard because the latter sorted first
- expectEmojiOrder('custard', 'star');
-
- const item = GfmAutoComplete.Emoji.insertTemplateFunction({ name: 'star' });
- expect(item).toEqual(`:${star.name}:`);
- });
- });
-
- describe('Emoji.templateFunction', () => {
- it('should map ":heart" to ❤ [regression]', () => {
- // the bug mapped heart to black_heart because the latter sorted first
- expectEmojiOrder('black_heart', 'heart');
-
- const item = GfmAutoComplete.Emoji.templateFunction('heart')
- .replace(/(<gl-emoji)\s+(data-name)/, '$1 $2')
- .replace(/>\s+|\s+</g, (s) => s.trim());
- expect(item).toEqual(
- `<li>${heart.name}<gl-emoji data-name="${heart.name}"></gl-emoji></li>`,
- );
- });
-
- it('should map ":star" to ⭐ [regression]', () => {
- // the bug mapped star to custard because the latter sorted first
- expectEmojiOrder('custard', 'star');
+ it('should return a correct template', () => {
+ const actual = GfmAutoComplete.Emoji.insertTemplateFunction(mockItem);
+ const expected = `:${mockItem.emoji.name}:`;
- const item = GfmAutoComplete.Emoji.templateFunction('star')
- .replace(/(<gl-emoji)\s+(data-name)/, '$1 $2')
- .replace(/>\s+|\s+</g, (s) => s.trim());
- expect(item).toEqual(`<li>${star.name}<gl-emoji data-name="${star.name}"></gl-emoji></li>`);
+ expect(actual).toBe(expected);
});
});
});
diff --git a/spec/frontend/gl_form_spec.js b/spec/frontend/gl_form_spec.js
index d9a01f7bcc1..07487fbb60e 100644
--- a/spec/frontend/gl_form_spec.js
+++ b/spec/frontend/gl_form_spec.js
@@ -1,5 +1,5 @@
-import $ from 'jquery';
import autosize from 'autosize';
+import $ from 'jquery';
import GLForm from '~/gl_form';
import '~/lib/utils/text_utility';
import '~/lib/utils/common_utils';
diff --git a/spec/frontend/gpg_badges_spec.js b/spec/frontend/gpg_badges_spec.js
index 7c1f83e577c..cd2cc88fa5a 100644
--- a/spec/frontend/gpg_badges_spec.js
+++ b/spec/frontend/gpg_badges_spec.js
@@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'spec/test_constants';
-import axios from '~/lib/utils/axios_utils';
import GpgBadges from '~/gpg_badges';
+import axios from '~/lib/utils/axios_utils';
describe('GpgBadges', () => {
let mock;
diff --git a/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap b/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap
index e880f585daa..0fc4343ec3c 100644
--- a/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap
+++ b/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap
@@ -8,13 +8,13 @@ exports[`grafana integration component default state to match the default snapsh
<div
class="settings-header"
>
- <h3
- class="js-section-header h4"
+ <h4
+ class="js-section-header"
>
Grafana authentication
- </h3>
+ </h4>
<gl-button-stub
buttontextclasses=""
diff --git a/spec/frontend/grafana_integration/components/grafana_integration_spec.js b/spec/frontend/grafana_integration/components/grafana_integration_spec.js
index df88a336c09..ad1260d8030 100644
--- a/spec/frontend/grafana_integration/components/grafana_integration_spec.js
+++ b/spec/frontend/grafana_integration/components/grafana_integration_spec.js
@@ -1,11 +1,11 @@
-import { mount, shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import GrafanaIntegration from '~/grafana_integration/components/grafana_integration.vue';
import { createStore } from '~/grafana_integration/store';
import axios from '~/lib/utils/axios_utils';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
jest.mock('~/lib/utils/url_utility');
jest.mock('~/flash');
diff --git a/spec/frontend/group_settings/components/shared_runners_form_spec.js b/spec/frontend/group_settings/components/shared_runners_form_spec.js
index 4ec739122c8..78950a8fe20 100644
--- a/spec/frontend/group_settings/components/shared_runners_form_spec.js
+++ b/spec/frontend/group_settings/components/shared_runners_form_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import MockAxiosAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import SharedRunnersForm from '~/group_settings/components/shared_runners_form.vue';
diff --git a/spec/frontend/groups/components/app_spec.js b/spec/frontend/groups/components/app_spec.js
index 9244e4f331e..e559c9519f2 100644
--- a/spec/frontend/groups/components/app_spec.js
+++ b/spec/frontend/groups/components/app_spec.js
@@ -1,16 +1,16 @@
import '~/flash';
-import Vue from 'vue';
-import AxiosMockAdapter from 'axios-mock-adapter';
import { GlModal, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import AxiosMockAdapter from 'axios-mock-adapter';
+import Vue from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import axios from '~/lib/utils/axios_utils';
import appComponent from '~/groups/components/app.vue';
import groupFolderComponent from '~/groups/components/group_folder.vue';
import groupItemComponent from '~/groups/components/group_item.vue';
import eventHub from '~/groups/event_hub';
-import GroupsStore from '~/groups/store/groups_store';
import GroupsService from '~/groups/service/groups_service';
+import GroupsStore from '~/groups/store/groups_store';
+import axios from '~/lib/utils/axios_utils';
import * as urlUtilities from '~/lib/utils/url_utility';
import {
@@ -60,8 +60,8 @@ describe('AppComponent', () => {
beforeEach(() => {
mock = new AxiosMockAdapter(axios);
mock.onGet('/dashboard/groups.json').reply(200, mockGroups);
- Vue.component('group-folder', groupFolderComponent);
- Vue.component('group-item', groupItemComponent);
+ Vue.component('GroupFolder', groupFolderComponent);
+ Vue.component('GroupItem', groupItemComponent);
createShallowComponent();
getGroupsSpy = jest.spyOn(vm.service, 'getGroups');
diff --git a/spec/frontend/groups/components/group_folder_spec.js b/spec/frontend/groups/components/group_folder_spec.js
index a40fa9bece8..1d8e10479b6 100644
--- a/spec/frontend/groups/components/group_folder_spec.js
+++ b/spec/frontend/groups/components/group_folder_spec.js
@@ -19,7 +19,7 @@ describe('GroupFolderComponent', () => {
let vm;
beforeEach(() => {
- Vue.component('group-item', groupItemComponent);
+ Vue.component('GroupItem', groupItemComponent);
vm = createComponent();
vm.$mount();
diff --git a/spec/frontend/groups/components/group_item_spec.js b/spec/frontend/groups/components/group_item_spec.js
index d70ea709dee..4fcc9bafa46 100644
--- a/spec/frontend/groups/components/group_item_spec.js
+++ b/spec/frontend/groups/components/group_item_spec.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
-import groupItemComponent from '~/groups/components/group_item.vue';
import groupFolderComponent from '~/groups/components/group_folder.vue';
-import { getGroupItemMicrodata } from '~/groups/store/utils';
+import groupItemComponent from '~/groups/components/group_item.vue';
import eventHub from '~/groups/event_hub';
+import { getGroupItemMicrodata } from '~/groups/store/utils';
import * as urlUtilities from '~/lib/utils/url_utility';
import { mockParentGroupItem, mockChildren } from '../mock_data';
@@ -20,7 +20,7 @@ describe('GroupItemComponent', () => {
let vm;
beforeEach(() => {
- Vue.component('group-folder', groupFolderComponent);
+ Vue.component('GroupFolder', groupFolderComponent);
vm = createComponent();
diff --git a/spec/frontend/groups/components/groups_spec.js b/spec/frontend/groups/components/groups_spec.js
index 6205400eb03..dc1a10639fc 100644
--- a/spec/frontend/groups/components/groups_spec.js
+++ b/spec/frontend/groups/components/groups_spec.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
-import groupsComponent from '~/groups/components/groups.vue';
import groupFolderComponent from '~/groups/components/group_folder.vue';
import groupItemComponent from '~/groups/components/group_item.vue';
+import groupsComponent from '~/groups/components/groups.vue';
import eventHub from '~/groups/event_hub';
import { mockGroups, mockPageInfo } from '../mock_data';
@@ -22,8 +22,8 @@ describe('GroupsComponent', () => {
let vm;
beforeEach(() => {
- Vue.component('group-folder', groupFolderComponent);
- Vue.component('group-item', groupItemComponent);
+ Vue.component('GroupFolder', groupFolderComponent);
+ Vue.component('GroupItem', groupItemComponent);
vm = createComponent();
diff --git a/spec/frontend/groups/components/invite_members_banner_spec.js b/spec/frontend/groups/components/invite_members_banner_spec.js
index 4e69f3cd433..9a2068a27a1 100644
--- a/spec/frontend/groups/components/invite_members_banner_spec.js
+++ b/spec/frontend/groups/components/invite_members_banner_spec.js
@@ -1,8 +1,8 @@
-import { shallowMount } from '@vue/test-utils';
import { GlBanner, GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
-import { setCookie, parseBoolean } from '~/lib/utils/common_utils';
import InviteMembersBanner from '~/groups/components/invite_members_banner.vue';
+import { setCookie, parseBoolean } from '~/lib/utils/common_utils';
jest.mock('~/lib/utils/common_utils');
diff --git a/spec/frontend/groups/components/item_actions_spec.js b/spec/frontend/groups/components/item_actions_spec.js
index 9adbc9abe13..ffbdf9b1aa6 100644
--- a/spec/frontend/groups/components/item_actions_spec.js
+++ b/spec/frontend/groups/components/item_actions_spec.js
@@ -66,6 +66,22 @@ describe('ItemActions', () => {
});
});
+ it('emits `showLeaveGroupModal` event with the correct prefix if `action` prop is passed', () => {
+ const group = {
+ ...mockParentGroupItem,
+ canEdit: true,
+ canLeave: true,
+ };
+ createComponent({
+ group,
+ action: 'test',
+ });
+ jest.spyOn(eventHub, '$emit');
+ findLeaveGroupBtn().vm.$emit('click', { stopPropagation: () => {} });
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('testshowLeaveGroupModal', group, parentGroup);
+ });
+
it('does not render leave button if group can not be left', () => {
createComponent({
group: {
diff --git a/spec/frontend/groups/components/item_caret_spec.js b/spec/frontend/groups/components/item_caret_spec.js
index b2915607a06..cbe1f21d6e2 100644
--- a/spec/frontend/groups/components/item_caret_spec.js
+++ b/spec/frontend/groups/components/item_caret_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import ItemCaret from '~/groups/components/item_caret.vue';
describe('ItemCaret', () => {
diff --git a/spec/frontend/groups/components/item_stats_value_spec.js b/spec/frontend/groups/components/item_stats_value_spec.js
index bca233883af..98186120a81 100644
--- a/spec/frontend/groups/components/item_stats_value_spec.js
+++ b/spec/frontend/groups/components/item_stats_value_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import ItemStatsValue from '~/groups/components/item_stats_value.vue';
describe('ItemStatsValue', () => {
diff --git a/spec/frontend/groups/components/item_type_icon_spec.js b/spec/frontend/groups/components/item_type_icon_spec.js
index 5e7056be218..9310943841e 100644
--- a/spec/frontend/groups/components/item_type_icon_spec.js
+++ b/spec/frontend/groups/components/item_type_icon_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import ItemTypeIcon from '~/groups/components/item_type_icon.vue';
import { ITEM_TYPE } from '../mock_data';
diff --git a/spec/frontend/groups/members/mock_data.js b/spec/frontend/groups/members/mock_data.js
deleted file mode 100644
index b84c9c6d446..00000000000
--- a/spec/frontend/groups/members/mock_data.js
+++ /dev/null
@@ -1,33 +0,0 @@
-export const membersJsonString =
- '[{"requested_at":null,"can_update":true,"can_remove":true,"can_override":false,"access_level":{"integer_value":50,"string_value":"Owner"},"source":{"id":323,"name":"My group / my subgroup","web_url":"http://127.0.0.1:3000/groups/my-group/my-subgroup"},"user":{"id":1,"name":"Administrator","username":"root","web_url":"http://127.0.0.1:3000/root","avatar_url":"https://www.gravatar.com/avatar/4816142ef496f956a277bedf1a40607b?s=80\u0026d=identicon","blocked":false,"two_factor_enabled":false},"id":524,"created_at":"2020-08-21T21:33:27.631Z","expires_at":null,"using_license":false,"group_sso":false,"group_managed_account":false}]';
-
-export const membersParsed = [
- {
- requestedAt: null,
- canUpdate: true,
- canRemove: true,
- canOverride: false,
- accessLevel: { integerValue: 50, stringValue: 'Owner' },
- source: {
- id: 323,
- name: 'My group / my subgroup',
- webUrl: 'http://127.0.0.1:3000/groups/my-group/my-subgroup',
- },
- user: {
- id: 1,
- name: 'Administrator',
- username: 'root',
- webUrl: 'http://127.0.0.1:3000/root',
- avatarUrl:
- 'https://www.gravatar.com/avatar/4816142ef496f956a277bedf1a40607b?s=80&d=identicon',
- blocked: false,
- twoFactorEnabled: false,
- },
- id: 524,
- createdAt: '2020-08-21T21:33:27.631Z',
- expiresAt: null,
- usingLicense: false,
- groupSso: false,
- groupManagedAccount: false,
- },
-];
diff --git a/spec/frontend/groups/members/utils_spec.js b/spec/frontend/groups/members/utils_spec.js
index 68945174e9d..0912e66e3e8 100644
--- a/spec/frontend/groups/members/utils_spec.js
+++ b/spec/frontend/groups/members/utils_spec.js
@@ -1,53 +1,14 @@
-import { membersJsonString, membersParsed } from './mock_data';
-import {
- parseDataAttributes,
- memberRequestFormatter,
- groupLinkRequestFormatter,
-} from '~/groups/members/utils';
+import { groupMemberRequestFormatter } from '~/groups/members/utils';
describe('group member utils', () => {
- describe('parseDataAttributes', () => {
- let el;
-
- beforeEach(() => {
- el = document.createElement('div');
- el.setAttribute('data-members', membersJsonString);
- el.setAttribute('data-group-id', '234');
- el.setAttribute('data-can-manage-members', 'true');
- });
-
- afterEach(() => {
- el = null;
- });
-
- it('correctly parses the data attributes', () => {
- expect(parseDataAttributes(el)).toEqual({
- members: membersParsed,
- sourceId: 234,
- canManageMembers: true,
- });
- });
- });
-
- describe('memberRequestFormatter', () => {
+ describe('groupMemberRequestFormatter', () => {
it('returns expected format', () => {
expect(
- memberRequestFormatter({
+ groupMemberRequestFormatter({
accessLevel: 50,
expires_at: '2020-10-16',
}),
).toEqual({ group_member: { access_level: 50, expires_at: '2020-10-16' } });
});
});
-
- describe('groupLinkRequestFormatter', () => {
- it('returns expected format', () => {
- expect(
- groupLinkRequestFormatter({
- accessLevel: 50,
- expires_at: '2020-10-16',
- }),
- ).toEqual({ group_link: { group_access: 50, expires_at: '2020-10-16' } });
- });
- });
});
diff --git a/spec/frontend/groups/service/groups_service_spec.js b/spec/frontend/groups/service/groups_service_spec.js
index 38a565eba01..e037a6df1e2 100644
--- a/spec/frontend/groups/service/groups_service_spec.js
+++ b/spec/frontend/groups/service/groups_service_spec.js
@@ -1,6 +1,6 @@
+import GroupsService from '~/groups/service/groups_service';
import axios from '~/lib/utils/axios_utils';
-import GroupsService from '~/groups/service/groups_service';
import { mockEndpoint, mockParentGroupItem } from '../mock_data';
describe('GroupsService', () => {
diff --git a/spec/frontend/ide/commit_icon_spec.js b/spec/frontend/ide/commit_icon_spec.js
index 0dfcae00298..3acdfec5393 100644
--- a/spec/frontend/ide/commit_icon_spec.js
+++ b/spec/frontend/ide/commit_icon_spec.js
@@ -1,6 +1,6 @@
+import getCommitIconMap from '~/ide/commit_icon';
import { commitItemIconMap } from '~/ide/constants';
import { decorateData } from '~/ide/stores/utils';
-import getCommitIconMap from '~/ide/commit_icon';
const createFile = (name = 'name', id = name, type = '', parent = null) =>
decorateData({
diff --git a/spec/frontend/ide/components/activity_bar_spec.js b/spec/frontend/ide/components/activity_bar_spec.js
index 1a4b6ca0b71..657817eb3d8 100644
--- a/spec/frontend/ide/components/activity_bar_spec.js
+++ b/spec/frontend/ide/components/activity_bar_spec.js
@@ -1,14 +1,16 @@
import Vue from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
-import { createStore } from '~/ide/stores';
-import { leftSidebarViews } from '~/ide/constants';
import ActivityBar from '~/ide/components/activity_bar.vue';
+import { leftSidebarViews } from '~/ide/constants';
+import { createStore } from '~/ide/stores';
describe('IDE activity bar', () => {
const Component = Vue.extend(ActivityBar);
let vm;
let store;
+ const findChangesBadge = () => vm.$el.querySelector('.badge');
+
beforeEach(() => {
store = createStore();
@@ -69,4 +71,19 @@ describe('IDE activity bar', () => {
});
});
});
+
+ describe('changes badge', () => {
+ it('is rendered when files are staged', () => {
+ store.state.stagedFiles = [{ path: '/path/to/file' }];
+ vm.$mount();
+
+ expect(findChangesBadge()).toBeTruthy();
+ expect(findChangesBadge().textContent.trim()).toBe('1');
+ });
+
+ it('is not rendered when no changes are present', () => {
+ vm.$mount();
+ expect(findChangesBadge()).toBeFalsy();
+ });
+ });
});
diff --git a/spec/frontend/ide/components/branches/item_spec.js b/spec/frontend/ide/components/branches/item_spec.js
index f1aa9187a8d..f90c298c401 100644
--- a/spec/frontend/ide/components/branches/item_spec.js
+++ b/spec/frontend/ide/components/branches/item_spec.js
@@ -1,8 +1,8 @@
-import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
-import { createStore } from '~/ide/stores';
-import { createRouter } from '~/ide/ide_router';
+import { shallowMount } from '@vue/test-utils';
import Item from '~/ide/components/branches/item.vue';
+import { createRouter } from '~/ide/ide_router';
+import { createStore } from '~/ide/stores';
import Timeago from '~/vue_shared/components/time_ago_tooltip.vue';
import { projectData } from '../../mock_data';
diff --git a/spec/frontend/ide/components/branches/search_list_spec.js b/spec/frontend/ide/components/branches/search_list_spec.js
index 85776f8cc0e..0efa7af2c6c 100644
--- a/spec/frontend/ide/components/branches/search_list_spec.js
+++ b/spec/frontend/ide/components/branches/search_list_spec.js
@@ -1,9 +1,9 @@
+import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import { GlLoadingIcon } from '@gitlab/ui';
-import { __ } from '~/locale';
-import List from '~/ide/components/branches/search_list.vue';
import Item from '~/ide/components/branches/item.vue';
+import List from '~/ide/components/branches/search_list.vue';
+import { __ } from '~/locale';
import { branches } from '../../mock_data';
const localVue = createLocalVue();
diff --git a/spec/frontend/ide/components/commit_sidebar/actions_spec.js b/spec/frontend/ide/components/commit_sidebar/actions_spec.js
index 91751bd34ea..c4dccf26af3 100644
--- a/spec/frontend/ide/components/commit_sidebar/actions_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/actions_spec.js
@@ -1,9 +1,12 @@
import Vue from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import { projectData, branches } from 'jest/ide/mock_data';
-import { createStore } from '~/ide/stores';
import commitActions from '~/ide/components/commit_sidebar/actions.vue';
-import consts from '~/ide/stores/modules/commit/constants';
+import { createStore } from '~/ide/stores';
+import {
+ COMMIT_TO_NEW_BRANCH,
+ COMMIT_TO_CURRENT_BRANCH,
+} from '~/ide/stores/modules/commit/constants';
const ACTION_UPDATE_COMMIT_ACTION = 'commit/updateCommitAction';
@@ -126,16 +129,16 @@ describe('IDE commit sidebar actions', () => {
it.each`
input | expectedOption
- ${{ currentBranchId: BRANCH_DEFAULT }} | ${consts.COMMIT_TO_NEW_BRANCH}
- ${{ currentBranchId: BRANCH_DEFAULT, emptyRepo: true }} | ${consts.COMMIT_TO_CURRENT_BRANCH}
- ${{ currentBranchId: BRANCH_PROTECTED, hasMR: true }} | ${consts.COMMIT_TO_CURRENT_BRANCH}
- ${{ currentBranchId: BRANCH_PROTECTED, hasMR: false }} | ${consts.COMMIT_TO_CURRENT_BRANCH}
- ${{ currentBranchId: BRANCH_PROTECTED_NO_ACCESS, hasMR: true }} | ${consts.COMMIT_TO_NEW_BRANCH}
- ${{ currentBranchId: BRANCH_PROTECTED_NO_ACCESS, hasMR: false }} | ${consts.COMMIT_TO_NEW_BRANCH}
- ${{ currentBranchId: BRANCH_REGULAR, hasMR: true }} | ${consts.COMMIT_TO_CURRENT_BRANCH}
- ${{ currentBranchId: BRANCH_REGULAR, hasMR: false }} | ${consts.COMMIT_TO_CURRENT_BRANCH}
- ${{ currentBranchId: BRANCH_REGULAR_NO_ACCESS, hasMR: true }} | ${consts.COMMIT_TO_NEW_BRANCH}
- ${{ currentBranchId: BRANCH_REGULAR_NO_ACCESS, hasMR: false }} | ${consts.COMMIT_TO_NEW_BRANCH}
+ ${{ currentBranchId: BRANCH_DEFAULT }} | ${COMMIT_TO_NEW_BRANCH}
+ ${{ currentBranchId: BRANCH_DEFAULT, emptyRepo: true }} | ${COMMIT_TO_CURRENT_BRANCH}
+ ${{ currentBranchId: BRANCH_PROTECTED, hasMR: true }} | ${COMMIT_TO_CURRENT_BRANCH}
+ ${{ currentBranchId: BRANCH_PROTECTED, hasMR: false }} | ${COMMIT_TO_CURRENT_BRANCH}
+ ${{ currentBranchId: BRANCH_PROTECTED_NO_ACCESS, hasMR: true }} | ${COMMIT_TO_NEW_BRANCH}
+ ${{ currentBranchId: BRANCH_PROTECTED_NO_ACCESS, hasMR: false }} | ${COMMIT_TO_NEW_BRANCH}
+ ${{ currentBranchId: BRANCH_REGULAR, hasMR: true }} | ${COMMIT_TO_CURRENT_BRANCH}
+ ${{ currentBranchId: BRANCH_REGULAR, hasMR: false }} | ${COMMIT_TO_CURRENT_BRANCH}
+ ${{ currentBranchId: BRANCH_REGULAR_NO_ACCESS, hasMR: true }} | ${COMMIT_TO_NEW_BRANCH}
+ ${{ currentBranchId: BRANCH_REGULAR_NO_ACCESS, hasMR: false }} | ${COMMIT_TO_NEW_BRANCH}
`(
'with $input, it dispatches update commit action with $expectedOption',
({ input, expectedOption }) => {
diff --git a/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js b/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js
index ff780939026..50635ffe894 100644
--- a/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js
@@ -1,7 +1,7 @@
-import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
-import { createStore } from '~/ide/stores';
+import Vuex from 'vuex';
import EditorHeader from '~/ide/components/commit_sidebar/editor_header.vue';
+import { createStore } from '~/ide/stores';
import { file } from '../../helpers';
const localVue = createLocalVue();
diff --git a/spec/frontend/ide/components/commit_sidebar/empty_state_spec.js b/spec/frontend/ide/components/commit_sidebar/empty_state_spec.js
index c1dab4a04b6..4f81c0aa5d3 100644
--- a/spec/frontend/ide/components/commit_sidebar/empty_state_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/empty_state_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
-import { createStore } from '~/ide/stores';
import emptyState from '~/ide/components/commit_sidebar/empty_state.vue';
+import { createStore } from '~/ide/stores';
describe('IDE commit panel empty state', () => {
let vm;
diff --git a/spec/frontend/ide/components/commit_sidebar/form_spec.js b/spec/frontend/ide/components/commit_sidebar/form_spec.js
index abd7e3bb8fc..2b567816ce8 100644
--- a/spec/frontend/ide/components/commit_sidebar/form_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/form_spec.js
@@ -1,11 +1,12 @@
+import { GlModal } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
-import { getByText } from '@testing-library/dom';
-import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
-import { projectData } from 'jest/ide/mock_data';
+import { stubComponent } from 'helpers/stub_component';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import waitForPromises from 'helpers/wait_for_promises';
-import { createStore } from '~/ide/stores';
-import consts from '~/ide/stores/modules/commit/constants';
+import { projectData } from 'jest/ide/mock_data';
import CommitForm from '~/ide/components/commit_sidebar/form.vue';
+import CommitMessageField from '~/ide/components/commit_sidebar/message_field.vue';
import { leftSidebarViews } from '~/ide/constants';
import {
createCodeownersCommitError,
@@ -13,258 +14,287 @@ import {
createBranchChangedCommitError,
branchAlreadyExistsCommitError,
} from '~/ide/lib/errors';
+import { createStore } from '~/ide/stores';
+import { COMMIT_TO_NEW_BRANCH } from '~/ide/stores/modules/commit/constants';
describe('IDE commit form', () => {
- const Component = Vue.extend(CommitForm);
- let vm;
+ let wrapper;
let store;
- const beginCommitButton = () => vm.$el.querySelector('[data-testid="begin-commit-button"]');
+ const createComponent = () => {
+ wrapper = shallowMount(CommitForm, {
+ store,
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
+ stubs: {
+ GlModal: stubComponent(GlModal),
+ },
+ });
+ };
+
+ const setLastCommitMessage = (msg) => {
+ store.state.lastCommitMsg = msg;
+ };
+ const goToCommitView = () => {
+ store.state.currentActivityView = leftSidebarViews.commit.name;
+ };
+ const goToEditView = () => {
+ store.state.currentActivityView = leftSidebarViews.edit.name;
+ };
+ const findBeginCommitButton = () => wrapper.find('[data-testid="begin-commit-button"]');
+ const findBeginCommitButtonTooltip = () =>
+ wrapper.find('[data-testid="begin-commit-button-tooltip"]');
+ const findBeginCommitButtonData = () => ({
+ disabled: findBeginCommitButton().props('disabled'),
+ tooltip: getBinding(findBeginCommitButtonTooltip().element, 'gl-tooltip').value.title,
+ });
+ const findCommitButton = () => wrapper.find('[data-testid="commit-button"]');
+ const findCommitButtonTooltip = () => wrapper.find('[data-testid="commit-button-tooltip"]');
+ const findCommitButtonData = () => ({
+ disabled: findCommitButton().props('disabled'),
+ tooltip: getBinding(findCommitButtonTooltip().element, 'gl-tooltip').value.title,
+ });
+ const clickCommitButton = () => findCommitButton().vm.$emit('click');
+ const findForm = () => wrapper.find('form');
+ const submitForm = () => findForm().trigger('submit');
+ const findCommitMessageInput = () => wrapper.find(CommitMessageField);
+ const setCommitMessageInput = (val) => findCommitMessageInput().vm.$emit('input', val);
+ const findDiscardDraftButton = () => wrapper.find('[data-testid="discard-draft"]');
beforeEach(() => {
store = createStore();
- store.state.changedFiles.push('test');
+ store.state.stagedFiles.push('test');
store.state.currentProjectId = 'abcproject';
store.state.currentBranchId = 'master';
- Vue.set(store.state.projects, 'abcproject', { ...projectData });
-
- vm = createComponentWithStore(Component, store).$mount();
+ Vue.set(store.state.projects, 'abcproject', {
+ ...projectData,
+ userPermissions: { pushCode: true },
+ });
});
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
- it('enables begin commit button when there are changes', () => {
- expect(beginCommitButton()).not.toHaveAttr('disabled');
- });
+ // Notes:
+ // - When there are no changes, there is no commit button so there's nothing to test :)
+ describe.each`
+ desc | stagedFiles | userPermissions | viewFn | buttonFn | disabled | tooltip
+ ${'when there are no changes'} | ${[]} | ${{ pushCode: true }} | ${goToEditView} | ${findBeginCommitButtonData} | ${true} | ${''}
+ ${'when there are changes'} | ${['test']} | ${{ pushCode: true }} | ${goToEditView} | ${findBeginCommitButtonData} | ${false} | ${''}
+ ${'when there are changes'} | ${['test']} | ${{ pushCode: true }} | ${goToCommitView} | ${findCommitButtonData} | ${false} | ${''}
+ ${'when user cannot push'} | ${['test']} | ${{ pushCode: false }} | ${goToEditView} | ${findBeginCommitButtonData} | ${true} | ${CommitForm.MSG_CANNOT_PUSH_CODE}
+ ${'when user cannot push'} | ${['test']} | ${{ pushCode: false }} | ${goToCommitView} | ${findCommitButtonData} | ${true} | ${CommitForm.MSG_CANNOT_PUSH_CODE}
+ `('$desc', ({ stagedFiles, userPermissions, viewFn, buttonFn, disabled, tooltip }) => {
+ beforeEach(async () => {
+ store.state.stagedFiles = stagedFiles;
+ store.state.projects.abcproject.userPermissions = userPermissions;
+
+ createComponent();
+ });
- it('disables begin commit button when there are no changes', async () => {
- store.state.changedFiles = [];
- await vm.$nextTick();
+ it(`at view=${viewFn.name}, ${buttonFn.name} has disabled=${disabled} tooltip=${tooltip}`, async () => {
+ viewFn();
- expect(beginCommitButton()).toHaveAttr('disabled');
+ await wrapper.vm.$nextTick();
+
+ expect(buttonFn()).toEqual({
+ disabled,
+ tooltip,
+ });
+ });
});
- describe('compact', () => {
- beforeEach(() => {
- vm.isCompact = true;
+ describe('on edit tab', () => {
+ beforeEach(async () => {
+ // Test that we react to switching to compact view.
+ goToCommitView();
+
+ createComponent();
- return vm.$nextTick();
+ goToEditView();
+
+ await wrapper.vm.$nextTick();
});
it('renders commit button in compact mode', () => {
- expect(beginCommitButton()).not.toBeNull();
- expect(beginCommitButton().textContent).toContain('Commit');
+ expect(findBeginCommitButton().exists()).toBe(true);
+ expect(findBeginCommitButton().text()).toBe('Commit…');
});
it('does not render form', () => {
- expect(vm.$el.querySelector('form')).toBeNull();
+ expect(findForm().exists()).toBe(false);
});
it('renders overview text', () => {
- vm.$store.state.stagedFiles.push('test');
-
- return vm.$nextTick(() => {
- expect(vm.$el.querySelector('p').textContent).toContain('1 changed file');
- });
+ expect(wrapper.find('p').text()).toBe('1 changed file');
});
- it('shows form when clicking commit button', () => {
- beginCommitButton().click();
-
- return vm.$nextTick(() => {
- expect(vm.$el.querySelector('form')).not.toBeNull();
- });
- });
+ it('when begin commit button is clicked, shows form', async () => {
+ findBeginCommitButton().vm.$emit('click');
- it('toggles activity bar view when clicking commit button', () => {
- beginCommitButton().click();
+ await wrapper.vm.$nextTick();
- return vm.$nextTick(() => {
- expect(store.state.currentActivityView).toBe(leftSidebarViews.commit.name);
- });
+ expect(findForm().exists()).toBe(true);
});
- it('collapses if lastCommitMsg is set to empty and current view is not commit view', async () => {
- store.state.lastCommitMsg = 'abc';
- store.state.currentActivityView = leftSidebarViews.edit.name;
- await vm.$nextTick();
-
- // if commit message is set, form is uncollapsed
- expect(vm.isCompact).toBe(false);
+ it('when begin commit button is clicked, sets activity view', async () => {
+ findBeginCommitButton().vm.$emit('click');
- store.state.lastCommitMsg = '';
- await vm.$nextTick();
+ await wrapper.vm.$nextTick();
- // collapsed when set to empty
- expect(vm.isCompact).toBe(true);
+ expect(store.state.currentActivityView).toBe(leftSidebarViews.commit.name);
});
- it('collapses if in commit view but there are no changes and vice versa', async () => {
- store.state.currentActivityView = leftSidebarViews.commit.name;
- await vm.$nextTick();
+ it('collapses if lastCommitMsg is set to empty and current view is not commit view', async () => {
+ // Test that it expands when lastCommitMsg is set
+ setLastCommitMessage('test');
+ goToEditView();
- // expanded by default if there are changes
- expect(vm.isCompact).toBe(false);
+ await wrapper.vm.$nextTick();
- store.state.changedFiles = [];
- await vm.$nextTick();
+ expect(findForm().exists()).toBe(true);
- expect(vm.isCompact).toBe(true);
+ // Now test that it collapses when lastCommitMsg is cleared
+ setLastCommitMessage('');
- store.state.changedFiles.push('test');
- await vm.$nextTick();
+ await wrapper.vm.$nextTick();
- // uncollapsed once again
- expect(vm.isCompact).toBe(false);
+ expect(findForm().exists()).toBe(false);
});
+ });
- it('collapses if switched from commit view to edit view and vice versa', async () => {
- store.state.currentActivityView = leftSidebarViews.edit.name;
- await vm.$nextTick();
-
- expect(vm.isCompact).toBe(true);
+ describe('on commit tab when window height is less than MAX_WINDOW_HEIGHT', () => {
+ let oldHeight;
- store.state.currentActivityView = leftSidebarViews.commit.name;
- await vm.$nextTick();
+ beforeEach(async () => {
+ oldHeight = window.innerHeight;
+ window.innerHeight = 700;
- expect(vm.isCompact).toBe(false);
+ createComponent();
- store.state.currentActivityView = leftSidebarViews.edit.name;
- await vm.$nextTick();
+ goToCommitView();
- expect(vm.isCompact).toBe(true);
+ await wrapper.vm.$nextTick();
});
- describe('when window height is less than MAX_WINDOW_HEIGHT', () => {
- let oldHeight;
-
- beforeEach(() => {
- oldHeight = window.innerHeight;
- window.innerHeight = 700;
- });
+ afterEach(() => {
+ window.innerHeight = oldHeight;
+ });
- afterEach(() => {
- window.innerHeight = oldHeight;
- });
+ it('stays collapsed if changes are added or removed', async () => {
+ expect(findForm().exists()).toBe(false);
- it('stays collapsed when switching from edit view to commit view and back', async () => {
- store.state.currentActivityView = leftSidebarViews.edit.name;
- await vm.$nextTick();
+ store.state.stagedFiles = [];
+ await wrapper.vm.$nextTick();
- expect(vm.isCompact).toBe(true);
+ expect(findForm().exists()).toBe(false);
- store.state.currentActivityView = leftSidebarViews.commit.name;
- await vm.$nextTick();
+ store.state.stagedFiles.push('test');
+ await wrapper.vm.$nextTick();
- expect(vm.isCompact).toBe(true);
+ expect(findForm().exists()).toBe(false);
+ });
+ });
- store.state.currentActivityView = leftSidebarViews.edit.name;
- await vm.$nextTick();
+ describe('on commit tab', () => {
+ beforeEach(async () => {
+ // Test that the component reacts to switching to full view
+ goToEditView();
- expect(vm.isCompact).toBe(true);
- });
+ createComponent();
- it('stays uncollapsed if changes are added or removed', async () => {
- store.state.currentActivityView = leftSidebarViews.commit.name;
- await vm.$nextTick();
+ goToCommitView();
- expect(vm.isCompact).toBe(true);
+ await wrapper.vm.$nextTick();
+ });
- store.state.changedFiles = [];
- await vm.$nextTick();
+ it('shows form', () => {
+ expect(findForm().exists()).toBe(true);
+ });
- expect(vm.isCompact).toBe(true);
+ it('hides begin commit button', () => {
+ expect(findBeginCommitButton().exists()).toBe(false);
+ });
- store.state.changedFiles.push('test');
- await vm.$nextTick();
+ describe('when no changed files', () => {
+ beforeEach(async () => {
+ store.state.stagedFiles = [];
+ await wrapper.vm.$nextTick();
+ });
- expect(vm.isCompact).toBe(true);
+ it('hides form', () => {
+ expect(findForm().exists()).toBe(false);
});
- it('uncollapses when clicked on Commit button in the edit view', async () => {
- store.state.currentActivityView = leftSidebarViews.edit.name;
- beginCommitButton().click();
- await waitForPromises();
+ it('expands again when staged files are added', async () => {
+ store.state.stagedFiles.push('test');
+ await wrapper.vm.$nextTick();
- expect(vm.isCompact).toBe(false);
+ expect(findForm().exists()).toBe(true);
});
});
- });
- describe('full', () => {
- beforeEach(() => {
- vm.isCompact = false;
+ it('updates commitMessage in store on input', async () => {
+ setCommitMessageInput('testing commit message');
- return vm.$nextTick();
+ await wrapper.vm.$nextTick();
+
+ expect(store.state.commit.commitMessage).toBe('testing commit message');
});
- it('updates commitMessage in store on input', () => {
- const textarea = vm.$el.querySelector('textarea');
+ describe('discard draft button', () => {
+ it('hidden when commitMessage is empty', () => {
+ expect(findDiscardDraftButton().exists()).toBe(false);
+ });
- textarea.value = 'testing commit message';
+ it('resets commitMessage when clicking discard button', async () => {
+ setCommitMessageInput('testing commit message');
- textarea.dispatchEvent(new Event('input'));
+ await wrapper.vm.$nextTick();
- return vm.$nextTick().then(() => {
- expect(vm.$store.state.commit.commitMessage).toBe('testing commit message');
- });
- });
+ expect(findCommitMessageInput().props('text')).toBe('testing commit message');
- it('updating currentActivityView not to commit view sets compact mode', () => {
- store.state.currentActivityView = 'a';
+ // Test that commitMessage is cleared on click
+ findDiscardDraftButton().vm.$emit('click');
- return vm.$nextTick(() => {
- expect(vm.isCompact).toBe(true);
+ await wrapper.vm.$nextTick();
+
+ expect(findCommitMessageInput().props('text')).toBe('');
});
});
- it('always opens itself in full view current activity view is not commit view when clicking commit button', () => {
- beginCommitButton().click();
+ describe('when submitting', () => {
+ beforeEach(async () => {
+ goToEditView();
- return vm.$nextTick(() => {
- expect(store.state.currentActivityView).toBe(leftSidebarViews.commit.name);
- expect(vm.isCompact).toBe(false);
- });
- });
+ createComponent();
- describe('discard draft button', () => {
- it('hidden when commitMessage is empty', () => {
- expect(vm.$el.querySelector('.btn-default').textContent).toContain('Collapse');
- });
+ goToCommitView();
+
+ await wrapper.vm.$nextTick();
- it('resets commitMessage when clicking discard button', () => {
- vm.$store.state.commit.commitMessage = 'testing commit message';
-
- return vm
- .$nextTick()
- .then(() => {
- vm.$el.querySelector('.btn-default').click();
- })
- .then(() => vm.$nextTick())
- .then(() => {
- expect(vm.$store.state.commit.commitMessage).not.toBe('testing commit message');
- });
+ setCommitMessageInput('testing commit message');
+
+ await wrapper.vm.$nextTick();
+
+ jest.spyOn(store, 'dispatch').mockResolvedValue();
});
- });
- describe('when submitting', () => {
- beforeEach(() => {
- jest.spyOn(vm, 'commitChanges');
+ it.each([clickCommitButton, submitForm])('when %p, commits changes', (fn) => {
+ fn();
- vm.$store.state.stagedFiles.push('test');
- vm.$store.state.commit.commitMessage = 'testing commit message';
+ expect(store.dispatch).toHaveBeenCalledWith('commit/commitChanges', undefined);
});
- it('calls commitChanges', () => {
- vm.commitChanges.mockResolvedValue({ success: true });
+ it('when cannot push code, submitting does nothing', async () => {
+ store.state.projects.abcproject.userPermissions.pushCode = false;
+ await wrapper.vm.$nextTick();
- return vm.$nextTick().then(() => {
- vm.$el.querySelector('.btn-success').click();
+ submitForm();
- expect(vm.commitChanges).toHaveBeenCalled();
- });
+ expect(store.dispatch).not.toHaveBeenCalled();
});
it.each`
@@ -272,31 +302,32 @@ describe('IDE commit form', () => {
${() => createCodeownersCommitError('test message')} | ${{ actionPrimary: { text: 'Create new branch' } }}
${createUnexpectedCommitError} | ${{ actionPrimary: null }}
`('opens error modal if commitError with $error', async ({ createError, props }) => {
- jest.spyOn(vm.$refs.commitErrorModal, 'show');
+ const modal = wrapper.find(GlModal);
+ modal.vm.show = jest.fn();
const error = createError();
store.state.commit.commitError = error;
- await vm.$nextTick();
+ await wrapper.vm.$nextTick();
- expect(vm.$refs.commitErrorModal.show).toHaveBeenCalled();
- expect(vm.$refs.commitErrorModal).toMatchObject({
+ expect(modal.vm.show).toHaveBeenCalled();
+ expect(modal.props()).toMatchObject({
actionCancel: { text: 'Cancel' },
...props,
});
// Because of the legacy 'mountComponent' approach here, the only way to
// test the text of the modal is by viewing the content of the modal added to the document.
- expect(document.body).toHaveText(error.messageHTML);
+ expect(modal.html()).toContain(error.messageHTML);
});
});
describe('with error modal with primary', () => {
beforeEach(() => {
- jest.spyOn(vm.$store, 'dispatch').mockReturnValue(Promise.resolve());
+ jest.spyOn(store, 'dispatch').mockResolvedValue();
});
const commitActions = [
- ['commit/updateCommitAction', consts.COMMIT_TO_NEW_BRANCH],
+ ['commit/updateCommitAction', COMMIT_TO_NEW_BRANCH],
['commit/commitChanges'],
];
@@ -310,27 +341,15 @@ describe('IDE commit form', () => {
async ({ commitError, expectedActions }) => {
store.state.commit.commitError = commitError('test message');
- await vm.$nextTick();
+ await wrapper.vm.$nextTick();
- getByText(document.body, 'Create new branch').click();
+ wrapper.find(GlModal).vm.$emit('ok');
await waitForPromises();
- expect(vm.$store.dispatch.mock.calls).toEqual(expectedActions);
+ expect(store.dispatch.mock.calls).toEqual(expectedActions);
},
);
});
});
-
- describe('commitButtonText', () => {
- it('returns commit text when staged files exist', () => {
- vm.$store.state.stagedFiles.push('testing');
-
- expect(vm.commitButtonText).toBe('Commit');
- });
-
- it('returns stage & commit text when staged files do not exist', () => {
- expect(vm.commitButtonText).toBe('Stage & Commit');
- });
- });
});
diff --git a/spec/frontend/ide/components/commit_sidebar/list_item_spec.js b/spec/frontend/ide/components/commit_sidebar/list_item_spec.js
index baa25a11c2a..b91ee88e0d6 100644
--- a/spec/frontend/ide/components/commit_sidebar/list_item_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/list_item_spec.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
import { trimText } from 'helpers/text_helper';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
-import { createStore } from '~/ide/stores';
import listItem from '~/ide/components/commit_sidebar/list_item.vue';
import { createRouter } from '~/ide/ide_router';
+import { createStore } from '~/ide/stores';
import { file } from '../../helpers';
describe('Multi-file editor commit sidebar list item', () => {
diff --git a/spec/frontend/ide/components/commit_sidebar/list_spec.js b/spec/frontend/ide/components/commit_sidebar/list_spec.js
index 898ec4bebce..eb12fc994a5 100644
--- a/spec/frontend/ide/components/commit_sidebar/list_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/list_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
-import { createStore } from '~/ide/stores';
import commitSidebarList from '~/ide/components/commit_sidebar/list.vue';
+import { createStore } from '~/ide/stores';
import { file } from '../../helpers';
describe('Multi-file editor commit sidebar list', () => {
diff --git a/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js b/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js
index 50da64abbbe..253c2a426ee 100644
--- a/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js
@@ -2,9 +2,12 @@ import Vue from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import { projectData, branches } from 'jest/ide/mock_data';
import NewMergeRequestOption from '~/ide/components/commit_sidebar/new_merge_request_option.vue';
-import { createStore } from '~/ide/stores';
import { PERMISSION_CREATE_MR } from '~/ide/constants';
-import consts from '~/ide/stores/modules/commit/constants';
+import { createStore } from '~/ide/stores';
+import {
+ COMMIT_TO_CURRENT_BRANCH,
+ COMMIT_TO_NEW_BRANCH,
+} from '~/ide/stores/modules/commit/constants';
describe('create new MR checkbox', () => {
let store;
@@ -27,8 +30,8 @@ describe('create new MR checkbox', () => {
vm = createComponentWithStore(Component, store);
vm.$store.state.commit.commitAction = createNewBranch
- ? consts.COMMIT_TO_NEW_BRANCH
- : consts.COMMIT_TO_CURRENT_BRANCH;
+ ? COMMIT_TO_NEW_BRANCH
+ : COMMIT_TO_CURRENT_BRANCH;
vm.$store.state.currentBranchId = currentBranchId;
diff --git a/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js b/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js
index 73d811f99b8..a6f3253321b 100644
--- a/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
-import { createStore } from '~/ide/stores';
import radioGroup from '~/ide/components/commit_sidebar/radio_group.vue';
+import { createStore } from '~/ide/stores';
describe('IDE commit sidebar radio group', () => {
let vm;
diff --git a/spec/frontend/ide/components/commit_sidebar/success_message_spec.js b/spec/frontend/ide/components/commit_sidebar/success_message_spec.js
index b116321efb3..7bbe47d37af 100644
--- a/spec/frontend/ide/components/commit_sidebar/success_message_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/success_message_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
-import { createStore } from '~/ide/stores';
import successMessage from '~/ide/components/commit_sidebar/success_message.vue';
+import { createStore } from '~/ide/stores';
describe('IDE commit panel successful commit state', () => {
let vm;
diff --git a/spec/frontend/ide/components/error_message_spec.js b/spec/frontend/ide/components/error_message_spec.js
index fa6816d3546..2de3fa863a8 100644
--- a/spec/frontend/ide/components/error_message_spec.js
+++ b/spec/frontend/ide/components/error_message_spec.js
@@ -1,6 +1,6 @@
+import { GlLoadingIcon } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import { GlLoadingIcon } from '@gitlab/ui';
import ErrorMessage from '~/ide/components/error_message.vue';
const localVue = createLocalVue();
diff --git a/spec/frontend/ide/components/file_row_extra_spec.js b/spec/frontend/ide/components/file_row_extra_spec.js
index 275b98bc154..641407c7b77 100644
--- a/spec/frontend/ide/components/file_row_extra_spec.js
+++ b/spec/frontend/ide/components/file_row_extra_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
-import { createStore } from '~/ide/stores';
import FileRowExtra from '~/ide/components/file_row_extra.vue';
+import { createStore } from '~/ide/stores';
import { file } from '../helpers';
describe('IDE extra file row component', () => {
diff --git a/spec/frontend/ide/components/file_templates/bar_spec.js b/spec/frontend/ide/components/file_templates/bar_spec.js
index 158995c9c9c..4ca99f8d055 100644
--- a/spec/frontend/ide/components/file_templates/bar_spec.js
+++ b/spec/frontend/ide/components/file_templates/bar_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
-import { createStore } from '~/ide/stores';
import Bar from '~/ide/components/file_templates/bar.vue';
+import { createStore } from '~/ide/stores';
import { file } from '../../helpers';
describe('IDE file templates bar component', () => {
diff --git a/spec/frontend/ide/components/file_templates/dropdown_spec.js b/spec/frontend/ide/components/file_templates/dropdown_spec.js
index 628580103a4..44ac9aa954d 100644
--- a/spec/frontend/ide/components/file_templates/dropdown_spec.js
+++ b/spec/frontend/ide/components/file_templates/dropdown_spec.js
@@ -1,7 +1,7 @@
-import Vuex from 'vuex';
-import $ from 'jquery';
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
+import $ from 'jquery';
+import Vuex from 'vuex';
import Dropdown from '~/ide/components/file_templates/dropdown.vue';
const localVue = createLocalVue();
diff --git a/spec/frontend/ide/components/ide_file_row_spec.js b/spec/frontend/ide/components/ide_file_row_spec.js
index c00ad7c8365..20c105460f2 100644
--- a/spec/frontend/ide/components/ide_file_row_spec.js
+++ b/spec/frontend/ide/components/ide_file_row_spec.js
@@ -1,9 +1,9 @@
import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex';
-import IdeFileRow from '~/ide/components/ide_file_row.vue';
-import FileRow from '~/vue_shared/components/file_row.vue';
import FileRowExtra from '~/ide/components/file_row_extra.vue';
+import IdeFileRow from '~/ide/components/ide_file_row.vue';
import { createStore } from '~/ide/stores';
+import FileRow from '~/vue_shared/components/file_row.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
diff --git a/spec/frontend/ide/components/ide_review_spec.js b/spec/frontend/ide/components/ide_review_spec.js
index 37f34a96495..740b7ada521 100644
--- a/spec/frontend/ide/components/ide_review_spec.js
+++ b/spec/frontend/ide/components/ide_review_spec.js
@@ -1,10 +1,10 @@
+import { createLocalVue, mount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
-import { createLocalVue, mount } from '@vue/test-utils';
-import { trimText } from 'helpers/text_helper';
import { keepAlive } from 'helpers/keep_alive_component_helper';
-import IdeReview from '~/ide/components/ide_review.vue';
+import { trimText } from 'helpers/text_helper';
import EditorModeDropdown from '~/ide/components/editor_mode_dropdown.vue';
+import IdeReview from '~/ide/components/ide_review.vue';
import { createStore } from '~/ide/stores';
import { file } from '../helpers';
import { projectData } from '../mock_data';
diff --git a/spec/frontend/ide/components/ide_side_bar_spec.js b/spec/frontend/ide/components/ide_side_bar_spec.js
index 72e9463945b..c683612b142 100644
--- a/spec/frontend/ide/components/ide_side_bar_spec.js
+++ b/spec/frontend/ide/components/ide_side_bar_spec.js
@@ -1,13 +1,13 @@
+import { GlSkeletonLoading } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import { GlSkeletonLoading } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
-import { createStore } from '~/ide/stores';
+import IdeReview from '~/ide/components/ide_review.vue';
import IdeSidebar from '~/ide/components/ide_side_bar.vue';
import IdeTree from '~/ide/components/ide_tree.vue';
import RepoCommitSection from '~/ide/components/repo_commit_section.vue';
-import IdeReview from '~/ide/components/ide_review.vue';
import { leftSidebarViews } from '~/ide/constants';
+import { createStore } from '~/ide/stores';
import { projectData } from '../mock_data';
const localVue = createLocalVue();
diff --git a/spec/frontend/ide/components/ide_sidebar_nav_spec.js b/spec/frontend/ide/components/ide_sidebar_nav_spec.js
index 6b4cb9bd03d..2ea0c250794 100644
--- a/spec/frontend/ide/components/ide_sidebar_nav_spec.js
+++ b/spec/frontend/ide/components/ide_sidebar_nav_spec.js
@@ -1,8 +1,9 @@
-import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import IdeSidebarNav from '~/ide/components/ide_sidebar_nav.vue';
import { SIDE_RIGHT, SIDE_LEFT } from '~/ide/constants';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
const TEST_TABS = [
{
@@ -74,7 +75,7 @@ describe('ide/components/ide_sidebar_nav', () => {
createComponent({ isOpen, side });
bsTooltipHide = jest.fn();
- wrapper.vm.$root.$on('bv::hide::tooltip', bsTooltipHide);
+ wrapper.vm.$root.$on(BV_HIDE_TOOLTIP, bsTooltipHide);
});
it('renders buttons', () => {
diff --git a/spec/frontend/ide/components/ide_spec.js b/spec/frontend/ide/components/ide_spec.js
index 805fa898611..c9d19c18d03 100644
--- a/spec/frontend/ide/components/ide_spec.js
+++ b/spec/frontend/ide/components/ide_spec.js
@@ -1,9 +1,10 @@
-import Vuex from 'vuex';
+import { GlAlert } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
+import Vuex from 'vuex';
import waitForPromises from 'helpers/wait_for_promises';
-import { createStore } from '~/ide/stores';
import ErrorMessage from '~/ide/components/error_message.vue';
-import ide from '~/ide/components/ide.vue';
+import Ide from '~/ide/components/ide.vue';
+import { createStore } from '~/ide/stores';
import { file } from '../helpers';
import { projectData } from '../mock_data';
@@ -15,12 +16,12 @@ describe('WebIDE', () => {
let wrapper;
- function createComponent({ projData = emptyProjData, state = {} } = {}) {
+ const createComponent = ({ projData = emptyProjData, state = {} } = {}) => {
const store = createStore();
store.state.currentProjectId = 'abcproject';
store.state.currentBranchId = 'master';
- store.state.projects.abcproject = { ...projData };
+ store.state.projects.abcproject = projData && { ...projData };
store.state.trees['abcproject/master'] = {
tree: [],
loading: false,
@@ -29,11 +30,13 @@ describe('WebIDE', () => {
store.state[key] = state[key];
});
- return shallowMount(ide, {
+ wrapper = shallowMount(Ide, {
store,
localVue,
});
- }
+ };
+
+ const findAlert = () => wrapper.find(GlAlert);
afterEach(() => {
wrapper.destroy();
@@ -42,7 +45,7 @@ describe('WebIDE', () => {
describe('ide component, empty repo', () => {
beforeEach(() => {
- wrapper = createComponent({
+ createComponent({
projData: {
empty_repo: true,
},
@@ -63,7 +66,7 @@ describe('WebIDE', () => {
`(
'should error message exists=$exists when errorMessage=$errorMessage',
async ({ errorMessage, exists }) => {
- wrapper = createComponent({
+ createComponent({
state: {
errorMessage,
},
@@ -78,12 +81,12 @@ describe('WebIDE', () => {
describe('onBeforeUnload', () => {
it('returns undefined when no staged files or changed files', () => {
- wrapper = createComponent();
+ createComponent();
expect(wrapper.vm.onBeforeUnload()).toBe(undefined);
});
it('returns warning text when their are changed files', () => {
- wrapper = createComponent({
+ createComponent({
state: {
changedFiles: [file()],
},
@@ -93,7 +96,7 @@ describe('WebIDE', () => {
});
it('returns warning text when their are staged files', () => {
- wrapper = createComponent({
+ createComponent({
state: {
stagedFiles: [file()],
},
@@ -104,7 +107,7 @@ describe('WebIDE', () => {
it('updates event object', () => {
const event = {};
- wrapper = createComponent({
+ createComponent({
state: {
stagedFiles: [file()],
},
@@ -118,7 +121,7 @@ describe('WebIDE', () => {
describe('non-existent branch', () => {
it('does not render "New file" button for non-existent branch when repo is not empty', () => {
- wrapper = createComponent({
+ createComponent({
state: {
projects: {},
},
@@ -130,7 +133,7 @@ describe('WebIDE', () => {
describe('branch with files', () => {
beforeEach(() => {
- wrapper = createComponent({
+ createComponent({
projData: {
empty_repo: false,
},
@@ -142,4 +145,31 @@ describe('WebIDE', () => {
});
});
});
+
+ it('when user cannot push code, shows alert', () => {
+ createComponent({
+ projData: {
+ userPermissions: {
+ pushCode: false,
+ },
+ },
+ });
+
+ expect(findAlert().props()).toMatchObject({
+ dismissible: false,
+ });
+ expect(findAlert().text()).toBe(Ide.MSG_CANNOT_PUSH_CODE);
+ });
+
+ it.each`
+ desc | projData
+ ${'when user can push code'} | ${{ userPermissions: { pushCode: true } }}
+ ${'when project is not ready'} | ${null}
+ `('$desc, no alert is shown', ({ projData }) => {
+ createComponent({
+ projData,
+ });
+
+ expect(findAlert().exists()).toBe(false);
+ });
});
diff --git a/spec/frontend/ide/components/ide_status_bar_spec.js b/spec/frontend/ide/components/ide_status_bar_spec.js
index 47506bb87e8..9d33a1e2554 100644
--- a/spec/frontend/ide/components/ide_status_bar_spec.js
+++ b/spec/frontend/ide/components/ide_status_bar_spec.js
@@ -1,10 +1,10 @@
-import Vue from 'vue';
import _ from 'lodash';
-import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
+import Vue from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
-import { createStore } from '~/ide/stores';
+import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import IdeStatusBar from '~/ide/components/ide_status_bar.vue';
import { rightSidebarViews } from '~/ide/constants';
+import { createStore } from '~/ide/stores';
import { projectData } from '../mock_data';
const TEST_PROJECT_ID = 'abcproject';
diff --git a/spec/frontend/ide/components/ide_status_list_spec.js b/spec/frontend/ide/components/ide_status_list_spec.js
index 02b5dc19bd8..036edfb3ec1 100644
--- a/spec/frontend/ide/components/ide_status_list_spec.js
+++ b/spec/frontend/ide/components/ide_status_list_spec.js
@@ -1,6 +1,6 @@
-import Vuex from 'vuex';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import Vuex from 'vuex';
import IdeStatusList from '~/ide/components/ide_status_list.vue';
import TerminalSyncStatusSafe from '~/ide/components/terminal_sync/terminal_sync_status_safe.vue';
diff --git a/spec/frontend/ide/components/ide_status_mr_spec.js b/spec/frontend/ide/components/ide_status_mr_spec.js
index ac1be4b21c0..0526d4653f8 100644
--- a/spec/frontend/ide/components/ide_status_mr_spec.js
+++ b/spec/frontend/ide/components/ide_status_mr_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlIcon, GlLink } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
import IdeStatusMr from '~/ide/components/ide_status_mr.vue';
diff --git a/spec/frontend/ide/components/ide_tree_spec.js b/spec/frontend/ide/components/ide_tree_spec.js
index c5934b032c4..6eef646b012 100644
--- a/spec/frontend/ide/components/ide_tree_spec.js
+++ b/spec/frontend/ide/components/ide_tree_spec.js
@@ -1,6 +1,6 @@
+import { mount, createLocalVue } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
-import { mount, createLocalVue } from '@vue/test-utils';
import { keepAlive } from 'helpers/keep_alive_component_helper';
import IdeTree from '~/ide/components/ide_tree.vue';
import { createStore } from '~/ide/stores';
diff --git a/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js b/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js
index b323ad8320c..d632a34266a 100644
--- a/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js
+++ b/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import ScrollButton from '~/ide/components/jobs/detail/scroll_button.vue';
describe('IDE job log scroll button', () => {
diff --git a/spec/frontend/ide/components/jobs/list_spec.js b/spec/frontend/ide/components/jobs/list_spec.js
index 5c5c6602374..8797e07aef1 100644
--- a/spec/frontend/ide/components/jobs/list_spec.js
+++ b/spec/frontend/ide/components/jobs/list_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount, mount, createLocalVue } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount, mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import StageList from '~/ide/components/jobs/list.vue';
import Stage from '~/ide/components/jobs/stage.vue';
diff --git a/spec/frontend/ide/components/jobs/stage_spec.js b/spec/frontend/ide/components/jobs/stage_spec.js
index e80215b9d13..9accd81a2ba 100644
--- a/spec/frontend/ide/components/jobs/stage_spec.js
+++ b/spec/frontend/ide/components/jobs/stage_spec.js
@@ -1,7 +1,7 @@
-import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
-import Stage from '~/ide/components/jobs/stage.vue';
+import { shallowMount } from '@vue/test-utils';
import Item from '~/ide/components/jobs/item.vue';
+import Stage from '~/ide/components/jobs/stage.vue';
import { stages, jobs } from '../../mock_data';
describe('IDE pipeline stage', () => {
diff --git a/spec/frontend/ide/components/merge_requests/item_spec.js b/spec/frontend/ide/components/merge_requests/item_spec.js
index 20adaa7abbc..f0a97a0b10a 100644
--- a/spec/frontend/ide/components/merge_requests/item_spec.js
+++ b/spec/frontend/ide/components/merge_requests/item_spec.js
@@ -1,8 +1,8 @@
-import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
-import { createStore } from '~/ide/stores';
-import { createRouter } from '~/ide/ide_router';
+import Vuex from 'vuex';
import Item from '~/ide/components/merge_requests/item.vue';
+import { createRouter } from '~/ide/ide_router';
+import { createStore } from '~/ide/stores';
const TEST_ITEM = {
iid: 1,
diff --git a/spec/frontend/ide/components/merge_requests/list_spec.js b/spec/frontend/ide/components/merge_requests/list_spec.js
index f0ac852fa67..85acabca38b 100644
--- a/spec/frontend/ide/components/merge_requests/list_spec.js
+++ b/spec/frontend/ide/components/merge_requests/list_spec.js
@@ -1,8 +1,8 @@
+import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import { GlLoadingIcon } from '@gitlab/ui';
-import List from '~/ide/components/merge_requests/list.vue';
import Item from '~/ide/components/merge_requests/item.vue';
+import List from '~/ide/components/merge_requests/list.vue';
import TokenedInput from '~/ide/components/shared/tokened_input.vue';
import { mergeRequests as mergeRequestsMock } from '../../mock_data';
diff --git a/spec/frontend/ide/components/nav_dropdown_spec.js b/spec/frontend/ide/components/nav_dropdown_spec.js
index 12a1a4c8013..4ddb3930764 100644
--- a/spec/frontend/ide/components/nav_dropdown_spec.js
+++ b/spec/frontend/ide/components/nav_dropdown_spec.js
@@ -1,8 +1,8 @@
-import $ from 'jquery';
import { mount } from '@vue/test-utils';
-import { createStore } from '~/ide/stores';
+import $ from 'jquery';
import NavDropdown from '~/ide/components/nav_dropdown.vue';
import { PERMISSION_READ_MR } from '~/ide/constants';
+import { createStore } from '~/ide/stores';
const TEST_PROJECT_ID = 'lorem-ipsum';
diff --git a/spec/frontend/ide/components/new_dropdown/index_spec.js b/spec/frontend/ide/components/new_dropdown/index_spec.js
index 793d950b3e0..5a1c0471206 100644
--- a/spec/frontend/ide/components/new_dropdown/index_spec.js
+++ b/spec/frontend/ide/components/new_dropdown/index_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
-import { createStore } from '~/ide/stores';
import newDropdown from '~/ide/components/new_dropdown/index.vue';
+import { createStore } from '~/ide/stores';
describe('new dropdown component', () => {
let store;
diff --git a/spec/frontend/ide/components/new_dropdown/modal_spec.js b/spec/frontend/ide/components/new_dropdown/modal_spec.js
index 06434081e39..0600fcea917 100644
--- a/spec/frontend/ide/components/new_dropdown/modal_spec.js
+++ b/spec/frontend/ide/components/new_dropdown/modal_spec.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
-import { createStore } from '~/ide/stores';
-import modal from '~/ide/components/new_dropdown/modal.vue';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import modal from '~/ide/components/new_dropdown/modal.vue';
+import { createStore } from '~/ide/stores';
jest.mock('~/flash');
diff --git a/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js b/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js
index c3da2a46858..7216f50b05c 100644
--- a/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js
+++ b/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js
@@ -1,9 +1,9 @@
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
+import IdeSidebarNav from '~/ide/components/ide_sidebar_nav.vue';
+import CollapsibleSidebar from '~/ide/components/panes/collapsible_sidebar.vue';
import { createStore } from '~/ide/stores';
import paneModule from '~/ide/stores/modules/pane';
-import CollapsibleSidebar from '~/ide/components/panes/collapsible_sidebar.vue';
-import IdeSidebarNav from '~/ide/components/ide_sidebar_nav.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
diff --git a/spec/frontend/ide/components/panes/right_spec.js b/spec/frontend/ide/components/panes/right_spec.js
index 57fd0e49e94..c6231d129ff 100644
--- a/spec/frontend/ide/components/panes/right_spec.js
+++ b/spec/frontend/ide/components/panes/right_spec.js
@@ -1,10 +1,10 @@
+import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
-import { createStore } from '~/ide/stores';
-import RightPane from '~/ide/components/panes/right.vue';
import CollapsibleSidebar from '~/ide/components/panes/collapsible_sidebar.vue';
+import RightPane from '~/ide/components/panes/right.vue';
import { rightSidebarViews } from '~/ide/constants';
+import { createStore } from '~/ide/stores';
import extendStore from '~/ide/stores/extend';
const localVue = createLocalVue();
diff --git a/spec/frontend/ide/components/pipelines/list_spec.js b/spec/frontend/ide/components/pipelines/list_spec.js
index a1fbfd96c31..58d8c0629fb 100644
--- a/spec/frontend/ide/components/pipelines/list_spec.js
+++ b/spec/frontend/ide/components/pipelines/list_spec.js
@@ -1,13 +1,13 @@
+import { GlLoadingIcon, GlTab } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
-import { GlLoadingIcon, GlTab } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants';
import { pipelines } from 'jest/ide/mock_data';
-import List from '~/ide/components/pipelines/list.vue';
import JobsList from '~/ide/components/jobs/list.vue';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import List from '~/ide/components/pipelines/list.vue';
import IDEServices from '~/ide/services';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
Vue.use(Vuex);
diff --git a/spec/frontend/ide/components/preview/clientside_spec.js b/spec/frontend/ide/components/preview/clientside_spec.js
index 220982e1fd9..1768f01f3b8 100644
--- a/spec/frontend/ide/components/preview/clientside_spec.js
+++ b/spec/frontend/ide/components/preview/clientside_spec.js
@@ -1,7 +1,7 @@
-import Vuex from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import smooshpack from 'smooshpack';
+import Vuex from 'vuex';
import Clientside from '~/ide/components/preview/clientside.vue';
import eventHub from '~/ide/eventhub';
diff --git a/spec/frontend/ide/components/preview/navigator_spec.js b/spec/frontend/ide/components/preview/navigator_spec.js
index ba5ac3bbbea..ee760364c7e 100644
--- a/spec/frontend/ide/components/preview/navigator_spec.js
+++ b/spec/frontend/ide/components/preview/navigator_spec.js
@@ -1,7 +1,7 @@
-import { shallowMount } from '@vue/test-utils';
-import { TEST_HOST } from 'helpers/test_constants';
import { GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { listen } from 'codesandbox-api';
+import { TEST_HOST } from 'helpers/test_constants';
import ClientsideNavigator from '~/ide/components/preview/navigator.vue';
jest.mock('codesandbox-api', () => ({
diff --git a/spec/frontend/ide/components/repo_commit_section_spec.js b/spec/frontend/ide/components/repo_commit_section_spec.js
index e91debee4ca..c174f5e2006 100644
--- a/spec/frontend/ide/components/repo_commit_section_spec.js
+++ b/spec/frontend/ide/components/repo_commit_section_spec.js
@@ -1,10 +1,10 @@
import { mount } from '@vue/test-utils';
import { keepAlive } from 'helpers/keep_alive_component_helper';
-import { createStore } from '~/ide/stores';
-import { createRouter } from '~/ide/ide_router';
-import RepoCommitSection from '~/ide/components/repo_commit_section.vue';
import EmptyState from '~/ide/components/commit_sidebar/empty_state.vue';
+import RepoCommitSection from '~/ide/components/repo_commit_section.vue';
import { stageKeys } from '~/ide/constants';
+import { createRouter } from '~/ide/ide_router';
+import { createStore } from '~/ide/stores';
import { file } from '../helpers';
const TEST_NO_CHANGES_SVG = 'nochangessvg';
diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js
index 89a7f423e34..1985feb1615 100644
--- a/spec/frontend/ide/components/repo_editor_spec.js
+++ b/spec/frontend/ide/components/repo_editor_spec.js
@@ -1,22 +1,22 @@
-import Vuex from 'vuex';
-import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
-import '~/behaviors/markdown/render_gfm';
import { Range } from 'monaco-editor';
+import Vue from 'vue';
+import Vuex from 'vuex';
+import '~/behaviors/markdown/render_gfm';
+import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import waitForPromises from 'helpers/wait_for_promises';
import waitUsingRealTimer from 'helpers/wait_using_real_timer';
-import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
-import axios from '~/lib/utils/axios_utils';
-import service from '~/ide/services';
-import { createStoreOptions } from '~/ide/stores';
import RepoEditor from '~/ide/components/repo_editor.vue';
-import Editor from '~/ide/lib/editor';
import {
leftSidebarViews,
FILE_VIEW_MODE_EDITOR,
FILE_VIEW_MODE_PREVIEW,
viewerTypes,
} from '~/ide/constants';
+import Editor from '~/ide/lib/editor';
+import service from '~/ide/services';
+import { createStoreOptions } from '~/ide/stores';
+import axios from '~/lib/utils/axios_utils';
import { file } from '../helpers';
import { exampleConfigs, exampleFiles } from '../lib/editorconfig/mock_data';
diff --git a/spec/frontend/ide/components/repo_tab_spec.js b/spec/frontend/ide/components/repo_tab_spec.js
index a44c8b4d5ee..b39a488b034 100644
--- a/spec/frontend/ide/components/repo_tab_spec.js
+++ b/spec/frontend/ide/components/repo_tab_spec.js
@@ -1,8 +1,8 @@
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import { createStore } from '~/ide/stores';
import RepoTab from '~/ide/components/repo_tab.vue';
import { createRouter } from '~/ide/ide_router';
+import { createStore } from '~/ide/stores';
import { file } from '../helpers';
const localVue = createLocalVue();
diff --git a/spec/frontend/ide/components/repo_tabs_spec.js b/spec/frontend/ide/components/repo_tabs_spec.js
index 45a17c37667..6ee73b0a437 100644
--- a/spec/frontend/ide/components/repo_tabs_spec.js
+++ b/spec/frontend/ide/components/repo_tabs_spec.js
@@ -1,7 +1,7 @@
-import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
-import { createStore } from '~/ide/stores';
+import Vuex from 'vuex';
import RepoTabs from '~/ide/components/repo_tabs.vue';
+import { createStore } from '~/ide/stores';
import { file } from '../helpers';
const localVue = createLocalVue();
diff --git a/spec/frontend/ide/components/resizable_panel_spec.js b/spec/frontend/ide/components/resizable_panel_spec.js
index b1a1212371e..6a5af52ea35 100644
--- a/spec/frontend/ide/components/resizable_panel_spec.js
+++ b/spec/frontend/ide/components/resizable_panel_spec.js
@@ -1,8 +1,8 @@
-import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
import ResizablePanel from '~/ide/components/resizable_panel.vue';
-import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
import { SIDE_LEFT, SIDE_RIGHT } from '~/ide/constants';
+import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
const TEST_WIDTH = 500;
const TEST_MIN_WIDTH = 400;
diff --git a/spec/frontend/ide/components/terminal/empty_state_spec.js b/spec/frontend/ide/components/terminal/empty_state_spec.js
index b62470f67b6..57c816747aa 100644
--- a/spec/frontend/ide/components/terminal/empty_state_spec.js
+++ b/spec/frontend/ide/components/terminal/empty_state_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon, GlButton, GlAlert } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'spec/test_constants';
import TerminalEmptyState from '~/ide/components/terminal/empty_state.vue';
diff --git a/spec/frontend/ide/components/terminal/session_spec.js b/spec/frontend/ide/components/terminal/session_spec.js
index 5653c8bf14d..5659a7d15da 100644
--- a/spec/frontend/ide/components/terminal/session_spec.js
+++ b/spec/frontend/ide/components/terminal/session_spec.js
@@ -1,5 +1,5 @@
-import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import TerminalSession from '~/ide/components/terminal/session.vue';
import Terminal from '~/ide/components/terminal/terminal.vue';
diff --git a/spec/frontend/ide/components/terminal/terminal_controls_spec.js b/spec/frontend/ide/components/terminal/terminal_controls_spec.js
index 99182710218..416096083f0 100644
--- a/spec/frontend/ide/components/terminal/terminal_controls_spec.js
+++ b/spec/frontend/ide/components/terminal/terminal_controls_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import TerminalControls from '~/ide/components/terminal/terminal_controls.vue';
import ScrollButton from '~/ide/components/jobs/detail/scroll_button.vue';
+import TerminalControls from '~/ide/components/terminal/terminal_controls.vue';
describe('IDE TerminalControls', () => {
let wrapper;
diff --git a/spec/frontend/ide/components/terminal/terminal_spec.js b/spec/frontend/ide/components/terminal/terminal_spec.js
index 9cfe167d2f6..c4b186c004a 100644
--- a/spec/frontend/ide/components/terminal/terminal_spec.js
+++ b/spec/frontend/ide/components/terminal/terminal_spec.js
@@ -1,7 +1,7 @@
-import Vue, { nextTick } from 'vue';
+import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
-import { GlLoadingIcon } from '@gitlab/ui';
import Terminal from '~/ide/components/terminal/terminal.vue';
import TerminalControls from '~/ide/components/terminal/terminal_controls.vue';
import {
diff --git a/spec/frontend/ide/components/terminal/view_spec.js b/spec/frontend/ide/components/terminal/view_spec.js
index 37f7957c526..e97d4d8a73b 100644
--- a/spec/frontend/ide/components/terminal/view_spec.js
+++ b/spec/frontend/ide/components/terminal/view_spec.js
@@ -1,10 +1,10 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import { TEST_HOST } from 'spec/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
+import { TEST_HOST } from 'spec/test_constants';
import TerminalEmptyState from '~/ide/components/terminal/empty_state.vue';
-import TerminalView from '~/ide/components/terminal/view.vue';
import TerminalSession from '~/ide/components/terminal/session.vue';
+import TerminalView from '~/ide/components/terminal/view.vue';
const TEST_HELP_PATH = `${TEST_HOST}/help`;
const TEST_SVG_PATH = `${TEST_HOST}/illustration.svg`;
diff --git a/spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js b/spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js
index afdecb7bbbd..69077ef2c68 100644
--- a/spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js
+++ b/spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js
@@ -1,5 +1,5 @@
-import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
+import Vuex from 'vuex';
import TerminalSyncStatus from '~/ide/components/terminal_sync/terminal_sync_status.vue';
import TerminalSyncStatusSafe from '~/ide/components/terminal_sync/terminal_sync_status_safe.vue';
diff --git a/spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js b/spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js
index d15583f81e4..c916c43d1e2 100644
--- a/spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js
+++ b/spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js
@@ -1,6 +1,6 @@
-import Vuex from 'vuex';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import Vuex from 'vuex';
import TerminalSyncStatus from '~/ide/components/terminal_sync/terminal_sync_status.vue';
import {
MSG_TERMINAL_SYNC_CONNECTING,
diff --git a/spec/frontend/ide/helpers.js b/spec/frontend/ide/helpers.js
index f815c3d090e..9b7a4715d7f 100644
--- a/spec/frontend/ide/helpers.js
+++ b/spec/frontend/ide/helpers.js
@@ -1,6 +1,6 @@
import * as pathUtils from 'path';
-import { decorateData } from '~/ide/stores/utils';
import { commitActionTypes } from '~/ide/constants';
+import { decorateData } from '~/ide/stores/utils';
export const file = (name = 'name', id = name, type = '', parent = null) =>
decorateData({
diff --git a/spec/frontend/ide/lib/create_diff_spec.js b/spec/frontend/ide/lib/create_diff_spec.js
index 76494f9af1b..b33fa599d1c 100644
--- a/spec/frontend/ide/lib/create_diff_spec.js
+++ b/spec/frontend/ide/lib/create_diff_spec.js
@@ -1,6 +1,6 @@
+import { commitActionTypes } from '~/ide/constants';
import createDiff from '~/ide/lib/create_diff';
import createFileDiff from '~/ide/lib/create_file_diff';
-import { commitActionTypes } from '~/ide/constants';
import {
createNewFile,
createUpdatedFile,
diff --git a/spec/frontend/ide/lib/create_file_diff_spec.js b/spec/frontend/ide/lib/create_file_diff_spec.js
index 294f0a926aa..646304c431b 100644
--- a/spec/frontend/ide/lib/create_file_diff_spec.js
+++ b/spec/frontend/ide/lib/create_file_diff_spec.js
@@ -1,5 +1,5 @@
-import createFileDiff from '~/ide/lib/create_file_diff';
import { commitActionTypes } from '~/ide/constants';
+import createFileDiff from '~/ide/lib/create_file_diff';
import {
createUpdatedFile,
createNewFile,
diff --git a/spec/frontend/ide/lib/decorations/controller_spec.js b/spec/frontend/ide/lib/decorations/controller_spec.js
index e9b7faaadfe..b513f1b2eba 100644
--- a/spec/frontend/ide/lib/decorations/controller_spec.js
+++ b/spec/frontend/ide/lib/decorations/controller_spec.js
@@ -1,8 +1,8 @@
-import Editor from '~/ide/lib/editor';
-import DecorationsController from '~/ide/lib/decorations/controller';
import Model from '~/ide/lib/common/model';
-import { file } from '../../helpers';
+import DecorationsController from '~/ide/lib/decorations/controller';
+import Editor from '~/ide/lib/editor';
import { createStore } from '~/ide/stores';
+import { file } from '../../helpers';
describe('Multi-file editor library decorations controller', () => {
let editorInstance;
diff --git a/spec/frontend/ide/lib/diff/controller_spec.js b/spec/frontend/ide/lib/diff/controller_spec.js
index 57c134620c0..5f1344f1ea2 100644
--- a/spec/frontend/ide/lib/diff/controller_spec.js
+++ b/spec/frontend/ide/lib/diff/controller_spec.js
@@ -1,9 +1,9 @@
import { Range } from 'monaco-editor';
-import Editor from '~/ide/lib/editor';
import ModelManager from '~/ide/lib/common/model_manager';
import DecorationsController from '~/ide/lib/decorations/controller';
import DirtyDiffController, { getDiffChangeType, getDecorator } from '~/ide/lib/diff/controller';
import { computeDiff } from '~/ide/lib/diff/diff';
+import Editor from '~/ide/lib/editor';
import { createStore } from '~/ide/stores';
import { file } from '../../helpers';
diff --git a/spec/frontend/ide/lib/editor_spec.js b/spec/frontend/ide/lib/editor_spec.js
index 12779c61dc3..c21a7edb2da 100644
--- a/spec/frontend/ide/lib/editor_spec.js
+++ b/spec/frontend/ide/lib/editor_spec.js
@@ -4,9 +4,10 @@ import {
Range,
Selection,
} from 'monaco-editor';
+import { EDITOR_TYPE_DIFF } from '~/editor/constants';
import Editor from '~/ide/lib/editor';
-import { createStore } from '~/ide/stores';
import { defaultEditorOptions } from '~/ide/lib/editor_options';
+import { createStore } from '~/ide/stores';
import { file } from '../helpers';
describe('Multi-file editor library', () => {
@@ -125,7 +126,7 @@ describe('Multi-file editor library', () => {
});
it('sets original & modified when diff editor', () => {
- jest.spyOn(instance.instance, 'getEditorType').mockReturnValue('vs.editor.IDiffEditor');
+ jest.spyOn(instance.instance, 'getEditorType').mockReturnValue(EDITOR_TYPE_DIFF);
jest.spyOn(instance.instance, 'setModel').mockImplementation(() => {});
instance.attachModel(model);
diff --git a/spec/frontend/ide/lib/languages/hcl_spec.js b/spec/frontend/ide/lib/languages/hcl_spec.js
index c6ebad6a4f4..948c44d4543 100644
--- a/spec/frontend/ide/lib/languages/hcl_spec.js
+++ b/spec/frontend/ide/lib/languages/hcl_spec.js
@@ -1,6 +1,6 @@
import { editor } from 'monaco-editor';
-import { registerLanguages } from '~/ide/utils';
import hcl from '~/ide/lib/languages/hcl';
+import { registerLanguages } from '~/ide/utils';
describe('tokenization for .tf files', () => {
beforeEach(() => {
diff --git a/spec/frontend/ide/lib/languages/vue_spec.js b/spec/frontend/ide/lib/languages/vue_spec.js
index ba5c31bb101..e3a67ccd47f 100644
--- a/spec/frontend/ide/lib/languages/vue_spec.js
+++ b/spec/frontend/ide/lib/languages/vue_spec.js
@@ -1,6 +1,6 @@
import { editor } from 'monaco-editor';
-import { registerLanguages } from '~/ide/utils';
import vue from '~/ide/lib/languages/vue';
+import { registerLanguages } from '~/ide/utils';
// This file only tests syntax specific to vue. This does not test existing syntaxes
// of html, javascript, css and handlebars, which vue files extend.
diff --git a/spec/frontend/ide/services/index_spec.js b/spec/frontend/ide/services/index_spec.js
index c3d6182bd78..678d58cba34 100644
--- a/spec/frontend/ide/services/index_spec.js
+++ b/spec/frontend/ide/services/index_spec.js
@@ -1,10 +1,10 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
-import services from '~/ide/services';
import Api from '~/api';
+import getUserPermissions from '~/ide/queries/getUserPermissions.query.graphql';
+import services from '~/ide/services';
import { query } from '~/ide/services/gql';
import { escapeFileUrl } from '~/lib/utils/url_utility';
-import getUserPermissions from '~/ide/queries/getUserPermissions.query.graphql';
import { projectData } from '../mock_data';
jest.mock('~/api');
diff --git a/spec/frontend/ide/stores/actions/file_spec.js b/spec/frontend/ide/stores/actions/file_spec.js
index 9d367714bbe..6178fb08d8c 100644
--- a/spec/frontend/ide/stores/actions/file_spec.js
+++ b/spec/frontend/ide/stores/actions/file_spec.js
@@ -1,12 +1,12 @@
-import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
+import Vue from 'vue';
+import eventHub from '~/ide/eventhub';
+import { createRouter } from '~/ide/ide_router';
+import service from '~/ide/services';
import { createStore } from '~/ide/stores';
import * as actions from '~/ide/stores/actions/file';
import * as types from '~/ide/stores/mutation_types';
-import service from '~/ide/services';
-import { createRouter } from '~/ide/ide_router';
-import eventHub from '~/ide/eventhub';
+import axios from '~/lib/utils/axios_utils';
import { file, createTriggerRenameAction, createTriggerUpdatePayload } from '../../helpers';
const ORIGINAL_CONTENT = 'original content';
diff --git a/spec/frontend/ide/stores/actions/merge_request_spec.js b/spec/frontend/ide/stores/actions/merge_request_spec.js
index 9b17d95ea35..600bd5fe9e1 100644
--- a/spec/frontend/ide/stores/actions/merge_request_spec.js
+++ b/spec/frontend/ide/stores/actions/merge_request_spec.js
@@ -1,19 +1,33 @@
import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import { createStore } from '~/ide/stores';
+import { range } from 'lodash';
+import { TEST_HOST } from 'helpers/test_constants';
+import testAction from 'helpers/vuex_action_helper';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import { leftSidebarViews, PERMISSION_READ_MR, MAX_MR_FILES_AUTO_OPEN } from '~/ide/constants';
+import service from '~/ide/services';
+import { createStore } from '~/ide/stores';
import {
getMergeRequestData,
getMergeRequestChanges,
getMergeRequestVersions,
+ openMergeRequestChanges,
openMergeRequest,
} from '~/ide/stores/actions/merge_request';
-import service from '~/ide/services';
-import { leftSidebarViews, PERMISSION_READ_MR } from '~/ide/constants';
+import * as types from '~/ide/stores/mutation_types';
+import axios from '~/lib/utils/axios_utils';
const TEST_PROJECT = 'abcproject';
const TEST_PROJECT_ID = 17;
+const createMergeRequestChange = (path) => ({
+ new_path: path,
+ path,
+});
+const createMergeRequestChangesCount = (n) =>
+ range(n).map((i) => createMergeRequestChange(`loremispum_${i}.md`));
+
+const testGetUrlForPath = (path) => `${TEST_HOST}/test/${path}`;
+
jest.mock('~/flash');
describe('IDE store merge request actions', () => {
@@ -353,6 +367,72 @@ describe('IDE store merge request actions', () => {
});
});
+ describe('openMergeRequestChanges', () => {
+ it.each`
+ desc | changes | entries
+ ${'with empty changes'} | ${[]} | ${{}}
+ ${'with changes not matching entries'} | ${[{ new_path: '123.md' }]} | ${{ '456.md': {} }}
+ `('$desc, does nothing', ({ changes, entries }) => {
+ const state = { entries };
+
+ return testAction({
+ action: openMergeRequestChanges,
+ state,
+ payload: changes,
+ expectedActions: [],
+ expectedMutations: [],
+ });
+ });
+
+ it('updates views and opens mr changes', () => {
+ // This is the payload sent to the action
+ const changesPayload = createMergeRequestChangesCount(15);
+
+ // Remove some items from the payload to use for entries
+ const changes = changesPayload.slice(1, 14);
+
+ const entries = changes.reduce(
+ (acc, { path }) => Object.assign(acc, { [path]: path, type: 'blob' }),
+ {},
+ );
+ const pathsToOpen = changes.slice(0, MAX_MR_FILES_AUTO_OPEN).map((x) => x.new_path);
+
+ return testAction({
+ action: openMergeRequestChanges,
+ state: { entries, getUrlForPath: testGetUrlForPath },
+ payload: changesPayload,
+ expectedActions: [
+ { type: 'updateActivityBarView', payload: leftSidebarViews.review.name },
+ // Only activates first file
+ { type: 'router/push', payload: testGetUrlForPath(pathsToOpen[0]) },
+ { type: 'setFileActive', payload: pathsToOpen[0] },
+ // Fetches data for other files
+ ...pathsToOpen.slice(1).map((path) => ({
+ type: 'getFileData',
+ payload: { path, makeFileActive: false },
+ })),
+ ...pathsToOpen.slice(1).map((path) => ({
+ type: 'getRawFileData',
+ payload: { path },
+ })),
+ ],
+ expectedMutations: [
+ ...changes.map((change) => ({
+ type: types.SET_FILE_MERGE_REQUEST_CHANGE,
+ payload: {
+ file: entries[change.new_path],
+ mrChange: change,
+ },
+ })),
+ ...pathsToOpen.map((path) => ({
+ type: types.TOGGLE_FILE_OPEN,
+ payload: path,
+ })),
+ ],
+ });
+ });
+ });
+
describe('openMergeRequest', () => {
const mr = {
projectId: TEST_PROJECT,
@@ -409,7 +489,6 @@ describe('IDE store merge request actions', () => {
case 'getFiles':
case 'getMergeRequestVersions':
case 'getBranchData':
- case 'setFileMrChange':
return Promise.resolve();
default:
return originalDispatch(type, payload);
@@ -445,6 +524,7 @@ describe('IDE store merge request actions', () => {
],
['getMergeRequestVersions', mr],
['getMergeRequestChanges', mr],
+ ['openMergeRequestChanges', testMergeRequestChanges.changes],
]);
})
.then(done)
@@ -454,9 +534,11 @@ describe('IDE store merge request actions', () => {
it('updates activity bar view and gets file data, if changes are found', (done) => {
store.state.entries.foo = {
type: 'blob',
+ path: 'foo',
};
store.state.entries.bar = {
type: 'blob',
+ path: 'bar',
};
testMergeRequestChanges.changes = [
@@ -467,24 +549,9 @@ describe('IDE store merge request actions', () => {
openMergeRequest({ state: store.state, dispatch: store.dispatch, getters: mockGetters }, mr)
.then(() => {
expect(store.dispatch).toHaveBeenCalledWith(
- 'updateActivityBarView',
- leftSidebarViews.review.name,
+ 'openMergeRequestChanges',
+ testMergeRequestChanges.changes,
);
-
- testMergeRequestChanges.changes.forEach((change, i) => {
- expect(store.dispatch).toHaveBeenCalledWith('setFileMrChange', {
- file: store.state.entries[change.new_path],
- mrChange: change,
- });
-
- expect(store.dispatch).toHaveBeenCalledWith('getFileData', {
- path: change.new_path,
- makeFileActive: i === 0,
- openFile: true,
- });
- });
-
- expect(store.state.openFiles.length).toBe(testMergeRequestChanges.changes.length);
})
.then(done)
.catch(done.fail);
diff --git a/spec/frontend/ide/stores/actions/project_spec.js b/spec/frontend/ide/stores/actions/project_spec.js
index 1244c8af91a..23ffb5ff56b 100644
--- a/spec/frontend/ide/stores/actions/project_spec.js
+++ b/spec/frontend/ide/stores/actions/project_spec.js
@@ -1,7 +1,8 @@
import MockAdapter from 'axios-mock-adapter';
-import testAction from 'helpers/vuex_action_helper';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
-import axios from '~/lib/utils/axios_utils';
+import testAction from 'helpers/vuex_action_helper';
+import api from '~/api';
+import service from '~/ide/services';
import { createStore } from '~/ide/stores';
import {
refreshLastCommitData,
@@ -12,8 +13,7 @@ import {
loadFile,
loadBranch,
} from '~/ide/stores/actions';
-import service from '~/ide/services';
-import api from '~/api';
+import axios from '~/lib/utils/axios_utils';
const TEST_PROJECT_ID = 'abc/def';
diff --git a/spec/frontend/ide/stores/actions/tree_spec.js b/spec/frontend/ide/stores/actions/tree_spec.js
index 7831df9f3a4..8de2188a5f4 100644
--- a/spec/frontend/ide/stores/actions/tree_spec.js
+++ b/spec/frontend/ide/stores/actions/tree_spec.js
@@ -1,12 +1,12 @@
import MockAdapter from 'axios-mock-adapter';
-import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'helpers/test_constants';
+import testAction from 'helpers/vuex_action_helper';
+import { createRouter } from '~/ide/ide_router';
+import service from '~/ide/services';
+import { createStore } from '~/ide/stores';
import { showTreeEntry, getFiles, setDirectoryData } from '~/ide/stores/actions/tree';
import * as types from '~/ide/stores/mutation_types';
import axios from '~/lib/utils/axios_utils';
-import { createStore } from '~/ide/stores';
-import service from '~/ide/services';
-import { createRouter } from '~/ide/ide_router';
import { file, createEntriesFromPaths } from '../../helpers';
describe('Multi-file store tree actions', () => {
diff --git a/spec/frontend/ide/stores/actions_spec.js b/spec/frontend/ide/stores/actions_spec.js
index 036bc91cd11..d47dd88dd47 100644
--- a/spec/frontend/ide/stores/actions_spec.js
+++ b/spec/frontend/ide/stores/actions_spec.js
@@ -1,8 +1,8 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import { visitUrl } from '~/lib/utils/url_utility';
-import { createStore } from '~/ide/stores';
+import eventHub from '~/ide/eventhub';
import { createRouter } from '~/ide/ide_router';
+import { createStore } from '~/ide/stores';
import {
stageAllChanges,
unstageAllChanges,
@@ -18,10 +18,10 @@ import {
createTempEntry,
discardAllChanges,
} from '~/ide/stores/actions';
-import axios from '~/lib/utils/axios_utils';
import * as types from '~/ide/stores/mutation_types';
+import axios from '~/lib/utils/axios_utils';
+import { visitUrl } from '~/lib/utils/url_utility';
import { file, createTriggerRenameAction, createTriggerChangeAction } from '../helpers';
-import eventHub from '~/ide/eventhub';
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
diff --git a/spec/frontend/ide/stores/getters_spec.js b/spec/frontend/ide/stores/getters_spec.js
index 1787f9e9361..450f5592026 100644
--- a/spec/frontend/ide/stores/getters_spec.js
+++ b/spec/frontend/ide/stores/getters_spec.js
@@ -1,6 +1,7 @@
import { TEST_HOST } from 'helpers/test_constants';
-import * as getters from '~/ide/stores/getters';
import { createStore } from '~/ide/stores';
+import * as getters from '~/ide/stores/getters';
+import { DEFAULT_PERMISSIONS } from '../../../../app/assets/javascripts/ide/constants';
import { file } from '../helpers';
const TEST_PROJECT_ID = 'test_project';
@@ -386,7 +387,9 @@ describe('IDE store getters', () => {
describe('findProjectPermissions', () => {
it('returns false if project not found', () => {
- expect(localStore.getters.findProjectPermissions(TEST_PROJECT_ID)).toEqual({});
+ expect(localStore.getters.findProjectPermissions(TEST_PROJECT_ID)).toEqual(
+ DEFAULT_PERMISSIONS,
+ );
});
it('finds permission in given project', () => {
diff --git a/spec/frontend/ide/stores/modules/branches/actions_spec.js b/spec/frontend/ide/stores/modules/branches/actions_spec.js
index b1c077c4082..135dbc1f746 100644
--- a/spec/frontend/ide/stores/modules/branches/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/branches/actions_spec.js
@@ -1,8 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import axios from '~/lib/utils/axios_utils';
-import state from '~/ide/stores/modules/branches/state';
-import * as types from '~/ide/stores/modules/branches/mutation_types';
import {
requestBranches,
receiveBranchesError,
@@ -10,6 +7,9 @@ import {
fetchBranches,
resetBranches,
} from '~/ide/stores/modules/branches/actions';
+import * as types from '~/ide/stores/modules/branches/mutation_types';
+import state from '~/ide/stores/modules/branches/state';
+import axios from '~/lib/utils/axios_utils';
import { branches, projectData } from '../../../mock_data';
describe('IDE branches actions', () => {
diff --git a/spec/frontend/ide/stores/modules/branches/mutations_spec.js b/spec/frontend/ide/stores/modules/branches/mutations_spec.js
index ddf55479be9..fd6006749d2 100644
--- a/spec/frontend/ide/stores/modules/branches/mutations_spec.js
+++ b/spec/frontend/ide/stores/modules/branches/mutations_spec.js
@@ -1,6 +1,6 @@
-import state from '~/ide/stores/modules/branches/state';
-import mutations from '~/ide/stores/modules/branches/mutations';
import * as types from '~/ide/stores/modules/branches/mutation_types';
+import mutations from '~/ide/stores/modules/branches/mutations';
+import state from '~/ide/stores/modules/branches/state';
import { branches } from '../../../mock_data';
describe('IDE branches mutations', () => {
diff --git a/spec/frontend/ide/stores/modules/clientside/actions_spec.js b/spec/frontend/ide/stores/modules/clientside/actions_spec.js
index 05627f8ed0e..c167d056039 100644
--- a/spec/frontend/ide/stores/modules/clientside/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/clientside/actions_spec.js
@@ -1,8 +1,8 @@
import MockAdapter from 'axios-mock-adapter';
-import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'helpers/test_constants';
-import axios from '~/lib/utils/axios_utils';
+import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/ide/stores/modules/clientside/actions';
+import axios from '~/lib/utils/axios_utils';
const TEST_PROJECT_URL = `${TEST_HOST}/lorem/ipsum`;
const TEST_USAGE_URL = `${TEST_PROJECT_URL}/usage_ping/web_ide_clientside_preview`;
diff --git a/spec/frontend/ide/stores/modules/commit/actions_spec.js b/spec/frontend/ide/stores/modules/commit/actions_spec.js
index 5be0e22a9fc..b124eb391f3 100644
--- a/spec/frontend/ide/stores/modules/commit/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/commit/actions_spec.js
@@ -1,17 +1,20 @@
-import { file } from 'jest/ide/helpers';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import { visitUrl } from '~/lib/utils/url_utility';
-import { createStore } from '~/ide/stores';
-import service from '~/ide/services';
-import { createRouter } from '~/ide/ide_router';
+import { file } from 'jest/ide/helpers';
+import { commitActionTypes, PERMISSION_CREATE_MR } from '~/ide/constants';
import eventHub from '~/ide/eventhub';
-import consts from '~/ide/stores/modules/commit/constants';
-import * as mutationTypes from '~/ide/stores/modules/commit/mutation_types';
-import * as actions from '~/ide/stores/modules/commit/actions';
+import { createRouter } from '~/ide/ide_router';
import { createUnexpectedCommitError } from '~/ide/lib/errors';
-import { commitActionTypes, PERMISSION_CREATE_MR } from '~/ide/constants';
+import service from '~/ide/services';
+import { createStore } from '~/ide/stores';
+import * as actions from '~/ide/stores/modules/commit/actions';
+import {
+ COMMIT_TO_CURRENT_BRANCH,
+ COMMIT_TO_NEW_BRANCH,
+} from '~/ide/stores/modules/commit/constants';
+import * as mutationTypes from '~/ide/stores/modules/commit/mutation_types';
+import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
@@ -425,12 +428,12 @@ describe('IDE commit module actions', () => {
});
it('resets stores commit actions', (done) => {
- store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH;
+ store.state.commit.commitAction = COMMIT_TO_NEW_BRANCH;
store
.dispatch('commit/commitChanges')
.then(() => {
- expect(store.state.commit.commitAction).not.toBe(consts.COMMIT_TO_NEW_BRANCH);
+ expect(store.state.commit.commitAction).not.toBe(COMMIT_TO_NEW_BRANCH);
})
.then(done)
.catch(done.fail);
@@ -450,7 +453,7 @@ describe('IDE commit module actions', () => {
it('redirects to new merge request page', (done) => {
jest.spyOn(eventHub, '$on').mockImplementation();
- store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH;
+ store.state.commit.commitAction = COMMIT_TO_NEW_BRANCH;
store.state.commit.shouldCreateMR = true;
store
@@ -468,7 +471,7 @@ describe('IDE commit module actions', () => {
it('does not redirect to new merge request page when shouldCreateMR is not checked', (done) => {
jest.spyOn(eventHub, '$on').mockImplementation();
- store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH;
+ store.state.commit.commitAction = COMMIT_TO_NEW_BRANCH;
store.state.commit.shouldCreateMR = false;
store
@@ -483,7 +486,7 @@ describe('IDE commit module actions', () => {
it('does not redirect to merge request page if shouldCreateMR is checked, but branch is the default branch', async () => {
jest.spyOn(eventHub, '$on').mockImplementation();
- store.state.commit.commitAction = consts.COMMIT_TO_CURRENT_BRANCH;
+ store.state.commit.commitAction = COMMIT_TO_CURRENT_BRANCH;
store.state.commit.shouldCreateMR = true;
await store.dispatch('commit/commitChanges');
diff --git a/spec/frontend/ide/stores/modules/commit/getters_spec.js b/spec/frontend/ide/stores/modules/commit/getters_spec.js
index 66ed51dbd13..0dc938bb637 100644
--- a/spec/frontend/ide/stores/modules/commit/getters_spec.js
+++ b/spec/frontend/ide/stores/modules/commit/getters_spec.js
@@ -1,6 +1,9 @@
-import commitState from '~/ide/stores/modules/commit/state';
+import {
+ COMMIT_TO_CURRENT_BRANCH,
+ COMMIT_TO_NEW_BRANCH,
+} from '~/ide/stores/modules/commit/constants';
import * as getters from '~/ide/stores/modules/commit/getters';
-import consts from '~/ide/stores/modules/commit/constants';
+import commitState from '~/ide/stores/modules/commit/state';
describe('IDE commit module getters', () => {
let state;
@@ -147,13 +150,13 @@ describe('IDE commit module getters', () => {
describe('isCreatingNewBranch', () => {
it('returns false if NOT creating a new branch', () => {
- state.commitAction = consts.COMMIT_TO_CURRENT_BRANCH;
+ state.commitAction = COMMIT_TO_CURRENT_BRANCH;
expect(getters.isCreatingNewBranch(state)).toBeFalsy();
});
it('returns true if creating a new branch', () => {
- state.commitAction = consts.COMMIT_TO_NEW_BRANCH;
+ state.commitAction = COMMIT_TO_NEW_BRANCH;
expect(getters.isCreatingNewBranch(state)).toBeTruthy();
});
diff --git a/spec/frontend/ide/stores/modules/commit/mutations_spec.js b/spec/frontend/ide/stores/modules/commit/mutations_spec.js
index 6393a70eac6..50342832d75 100644
--- a/spec/frontend/ide/stores/modules/commit/mutations_spec.js
+++ b/spec/frontend/ide/stores/modules/commit/mutations_spec.js
@@ -1,6 +1,6 @@
-import commitState from '~/ide/stores/modules/commit/state';
-import mutations from '~/ide/stores/modules/commit/mutations';
import * as types from '~/ide/stores/modules/commit/mutation_types';
+import mutations from '~/ide/stores/modules/commit/mutations';
+import commitState from '~/ide/stores/modules/commit/state';
describe('IDE commit module mutations', () => {
let state;
diff --git a/spec/frontend/ide/stores/modules/editor/actions_spec.js b/spec/frontend/ide/stores/modules/editor/actions_spec.js
index 6a420ac32de..f006018364b 100644
--- a/spec/frontend/ide/stores/modules/editor/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/editor/actions_spec.js
@@ -1,6 +1,6 @@
import testAction from 'helpers/vuex_action_helper';
-import * as types from '~/ide/stores/modules/editor/mutation_types';
import * as actions from '~/ide/stores/modules/editor/actions';
+import * as types from '~/ide/stores/modules/editor/mutation_types';
import { createTriggerRenamePayload } from '../../../helpers';
describe('~/ide/stores/modules/editor/actions', () => {
diff --git a/spec/frontend/ide/stores/modules/editor/getters_spec.js b/spec/frontend/ide/stores/modules/editor/getters_spec.js
index 55e1e31f66f..14099cdaeb2 100644
--- a/spec/frontend/ide/stores/modules/editor/getters_spec.js
+++ b/spec/frontend/ide/stores/modules/editor/getters_spec.js
@@ -1,5 +1,5 @@
-import { createDefaultFileEditor } from '~/ide/stores/modules/editor/utils';
import * as getters from '~/ide/stores/modules/editor/getters';
+import { createDefaultFileEditor } from '~/ide/stores/modules/editor/utils';
const TEST_PATH = 'test/path.md';
const TEST_FILE_EDITOR = {
diff --git a/spec/frontend/ide/stores/modules/editor/mutations_spec.js b/spec/frontend/ide/stores/modules/editor/mutations_spec.js
index e4b330b3174..35d13f375a3 100644
--- a/spec/frontend/ide/stores/modules/editor/mutations_spec.js
+++ b/spec/frontend/ide/stores/modules/editor/mutations_spec.js
@@ -1,6 +1,6 @@
-import { createDefaultFileEditor } from '~/ide/stores/modules/editor/utils';
import * as types from '~/ide/stores/modules/editor/mutation_types';
import mutations from '~/ide/stores/modules/editor/mutations';
+import { createDefaultFileEditor } from '~/ide/stores/modules/editor/utils';
import { createTriggerRenamePayload } from '../../../helpers';
const TEST_PATH = 'test/path.md';
diff --git a/spec/frontend/ide/stores/modules/file_templates/actions_spec.js b/spec/frontend/ide/stores/modules/file_templates/actions_spec.js
index 76898e83c7a..9ff950b0875 100644
--- a/spec/frontend/ide/stores/modules/file_templates/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/file_templates/actions_spec.js
@@ -1,9 +1,9 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import axios from '~/lib/utils/axios_utils';
-import createState from '~/ide/stores/modules/file_templates/state';
import * as actions from '~/ide/stores/modules/file_templates/actions';
import * as types from '~/ide/stores/modules/file_templates/mutation_types';
+import createState from '~/ide/stores/modules/file_templates/state';
+import axios from '~/lib/utils/axios_utils';
describe('IDE file templates actions', () => {
let state;
diff --git a/spec/frontend/ide/stores/modules/file_templates/getters_spec.js b/spec/frontend/ide/stores/modules/file_templates/getters_spec.js
index c9676b23fa1..e237b167f96 100644
--- a/spec/frontend/ide/stores/modules/file_templates/getters_spec.js
+++ b/spec/frontend/ide/stores/modules/file_templates/getters_spec.js
@@ -1,6 +1,6 @@
-import createState from '~/ide/stores/state';
import { leftSidebarViews } from '~/ide/constants';
import * as getters from '~/ide/stores/modules/file_templates/getters';
+import createState from '~/ide/stores/state';
describe('IDE file templates getters', () => {
describe('templateTypes', () => {
diff --git a/spec/frontend/ide/stores/modules/file_templates/mutations_spec.js b/spec/frontend/ide/stores/modules/file_templates/mutations_spec.js
index 6a1a826093c..3ea3c9507dd 100644
--- a/spec/frontend/ide/stores/modules/file_templates/mutations_spec.js
+++ b/spec/frontend/ide/stores/modules/file_templates/mutations_spec.js
@@ -1,6 +1,6 @@
-import createState from '~/ide/stores/modules/file_templates/state';
import * as types from '~/ide/stores/modules/file_templates/mutation_types';
import mutations from '~/ide/stores/modules/file_templates/mutations';
+import createState from '~/ide/stores/modules/file_templates/state';
const mockFileTemplates = [['MIT'], ['CC']];
const mockTemplateType = 'test';
diff --git a/spec/frontend/ide/stores/modules/merge_requests/actions_spec.js b/spec/frontend/ide/stores/modules/merge_requests/actions_spec.js
index 6594d65f558..e1f2b165dd9 100644
--- a/spec/frontend/ide/stores/modules/merge_requests/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/merge_requests/actions_spec.js
@@ -1,8 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import axios from '~/lib/utils/axios_utils';
-import state from '~/ide/stores/modules/merge_requests/state';
-import * as types from '~/ide/stores/modules/merge_requests/mutation_types';
import {
requestMergeRequests,
receiveMergeRequestsError,
@@ -10,6 +7,9 @@ import {
fetchMergeRequests,
resetMergeRequests,
} from '~/ide/stores/modules/merge_requests/actions';
+import * as types from '~/ide/stores/modules/merge_requests/mutation_types';
+import state from '~/ide/stores/modules/merge_requests/state';
+import axios from '~/lib/utils/axios_utils';
import { mergeRequests } from '../../../mock_data';
describe('IDE merge requests actions', () => {
diff --git a/spec/frontend/ide/stores/modules/merge_requests/mutations_spec.js b/spec/frontend/ide/stores/modules/merge_requests/mutations_spec.js
index d33bda3652d..f45c577f801 100644
--- a/spec/frontend/ide/stores/modules/merge_requests/mutations_spec.js
+++ b/spec/frontend/ide/stores/modules/merge_requests/mutations_spec.js
@@ -1,7 +1,7 @@
import { TEST_HOST } from 'helpers/test_constants';
-import state from '~/ide/stores/modules/merge_requests/state';
-import mutations from '~/ide/stores/modules/merge_requests/mutations';
import * as types from '~/ide/stores/modules/merge_requests/mutation_types';
+import mutations from '~/ide/stores/modules/merge_requests/mutations';
+import state from '~/ide/stores/modules/merge_requests/state';
import { mergeRequests } from '../../../mock_data';
describe('IDE merge requests mutations', () => {
diff --git a/spec/frontend/ide/stores/modules/pane/mutations_spec.js b/spec/frontend/ide/stores/modules/pane/mutations_spec.js
index b5fcd35912e..eaeb2c8cd28 100644
--- a/spec/frontend/ide/stores/modules/pane/mutations_spec.js
+++ b/spec/frontend/ide/stores/modules/pane/mutations_spec.js
@@ -1,6 +1,6 @@
-import state from '~/ide/stores/modules/pane/state';
-import mutations from '~/ide/stores/modules/pane/mutations';
import * as types from '~/ide/stores/modules/pane/mutation_types';
+import mutations from '~/ide/stores/modules/pane/mutations';
+import state from '~/ide/stores/modules/pane/state';
describe('IDE pane module mutations', () => {
const TEST_VIEW = 'test-view';
diff --git a/spec/frontend/ide/stores/modules/pipelines/actions_spec.js b/spec/frontend/ide/stores/modules/pipelines/actions_spec.js
index b7ed257e954..9aa31136c89 100644
--- a/spec/frontend/ide/stores/modules/pipelines/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/pipelines/actions_spec.js
@@ -1,8 +1,8 @@
-import Visibility from 'visibilityjs';
import MockAdapter from 'axios-mock-adapter';
+import Visibility from 'visibilityjs';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
-import axios from '~/lib/utils/axios_utils';
+import { rightSidebarViews } from '~/ide/constants';
import {
requestLatestPipeline,
receiveLatestPipelineError,
@@ -22,9 +22,9 @@ import {
fetchJobLogs,
resetLatestPipeline,
} from '~/ide/stores/modules/pipelines/actions';
-import state from '~/ide/stores/modules/pipelines/state';
import * as types from '~/ide/stores/modules/pipelines/mutation_types';
-import { rightSidebarViews } from '~/ide/constants';
+import state from '~/ide/stores/modules/pipelines/state';
+import axios from '~/lib/utils/axios_utils';
import { pipelines, jobs } from '../../../mock_data';
describe('IDE pipelines actions', () => {
diff --git a/spec/frontend/ide/stores/modules/pipelines/mutations_spec.js b/spec/frontend/ide/stores/modules/pipelines/mutations_spec.js
index d820bf0291e..0e738b98918 100644
--- a/spec/frontend/ide/stores/modules/pipelines/mutations_spec.js
+++ b/spec/frontend/ide/stores/modules/pipelines/mutations_spec.js
@@ -1,6 +1,6 @@
+import * as types from '~/ide/stores/modules/pipelines/mutation_types';
import mutations from '~/ide/stores/modules/pipelines/mutations';
import state from '~/ide/stores/modules/pipelines/state';
-import * as types from '~/ide/stores/modules/pipelines/mutation_types';
import { fullPipelinesResponse, stages, jobs } from '../../../mock_data';
describe('IDE pipelines mutations', () => {
diff --git a/spec/frontend/ide/stores/modules/router/mutations_spec.js b/spec/frontend/ide/stores/modules/router/mutations_spec.js
index a4a83c9344d..5a9f266db94 100644
--- a/spec/frontend/ide/stores/modules/router/mutations_spec.js
+++ b/spec/frontend/ide/stores/modules/router/mutations_spec.js
@@ -1,5 +1,5 @@
-import mutations from '~/ide/stores/modules/router/mutations';
import * as types from '~/ide/stores/modules/router/mutation_types';
+import mutations from '~/ide/stores/modules/router/mutations';
import createState from '~/ide/stores/modules/router/state';
const TEST_PATH = 'test/path/abc';
diff --git a/spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js b/spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js
index 05935f1db38..e5887ca0a33 100644
--- a/spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js
+++ b/spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js
@@ -1,14 +1,14 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'spec/test_constants';
+import * as actions from '~/ide/stores/modules/terminal/actions/checks';
import {
CHECK_CONFIG,
CHECK_RUNNERS,
RETRY_RUNNERS_INTERVAL,
} from '~/ide/stores/modules/terminal/constants';
-import * as mutationTypes from '~/ide/stores/modules/terminal/mutation_types';
import * as messages from '~/ide/stores/modules/terminal/messages';
-import * as actions from '~/ide/stores/modules/terminal/actions/checks';
+import * as mutationTypes from '~/ide/stores/modules/terminal/mutation_types';
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
diff --git a/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js b/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js
index dd51786745f..e42e760b841 100644
--- a/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js
+++ b/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js
@@ -1,12 +1,12 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+import * as actions from '~/ide/stores/modules/terminal/actions/session_controls';
import { STARTING, PENDING, STOPPING, STOPPED } from '~/ide/stores/modules/terminal/constants';
import * as messages from '~/ide/stores/modules/terminal/messages';
import * as mutationTypes from '~/ide/stores/modules/terminal/mutation_types';
-import * as actions from '~/ide/stores/modules/terminal/actions/session_controls';
-import httpStatus from '~/lib/utils/http_status';
import axios from '~/lib/utils/axios_utils';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import httpStatus from '~/lib/utils/http_status';
jest.mock('~/flash');
diff --git a/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js b/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js
index 0e123dce798..0227955754c 100644
--- a/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js
+++ b/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js
@@ -1,11 +1,11 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+import * as actions from '~/ide/stores/modules/terminal/actions/session_status';
import { PENDING, RUNNING, STOPPING, STOPPED } from '~/ide/stores/modules/terminal/constants';
import * as messages from '~/ide/stores/modules/terminal/messages';
import * as mutationTypes from '~/ide/stores/modules/terminal/mutation_types';
-import * as actions from '~/ide/stores/modules/terminal/actions/session_status';
import axios from '~/lib/utils/axios_utils';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
jest.mock('~/flash');
diff --git a/spec/frontend/ide/stores/modules/terminal/actions/setup_spec.js b/spec/frontend/ide/stores/modules/terminal/actions/setup_spec.js
index 8bf3b58228e..a823c05c459 100644
--- a/spec/frontend/ide/stores/modules/terminal/actions/setup_spec.js
+++ b/spec/frontend/ide/stores/modules/terminal/actions/setup_spec.js
@@ -1,6 +1,6 @@
import testAction from 'helpers/vuex_action_helper';
-import * as mutationTypes from '~/ide/stores/modules/terminal/mutation_types';
import * as actions from '~/ide/stores/modules/terminal/actions/setup';
+import * as mutationTypes from '~/ide/stores/modules/terminal/mutation_types';
describe('IDE store terminal setup actions', () => {
describe('init', () => {
diff --git a/spec/frontend/ide/stores/modules/terminal/messages_spec.js b/spec/frontend/ide/stores/modules/terminal/messages_spec.js
index 1bb92a9dfa5..e8f375a70b5 100644
--- a/spec/frontend/ide/stores/modules/terminal/messages_spec.js
+++ b/spec/frontend/ide/stores/modules/terminal/messages_spec.js
@@ -1,8 +1,8 @@
import { escape } from 'lodash';
import { TEST_HOST } from 'spec/test_constants';
import * as messages from '~/ide/stores/modules/terminal/messages';
-import { sprintf } from '~/locale';
import httpStatus from '~/lib/utils/http_status';
+import { sprintf } from '~/locale';
const TEST_HELP_URL = `${TEST_HOST}/help`;
diff --git a/spec/frontend/ide/stores/modules/terminal/mutations_spec.js b/spec/frontend/ide/stores/modules/terminal/mutations_spec.js
index e9933bdd7be..3451932a185 100644
--- a/spec/frontend/ide/stores/modules/terminal/mutations_spec.js
+++ b/spec/frontend/ide/stores/modules/terminal/mutations_spec.js
@@ -4,9 +4,9 @@ import {
RUNNING,
STOPPING,
} from '~/ide/stores/modules/terminal/constants';
-import createState from '~/ide/stores/modules/terminal/state';
import * as types from '~/ide/stores/modules/terminal/mutation_types';
import mutations from '~/ide/stores/modules/terminal/mutations';
+import createState from '~/ide/stores/modules/terminal/state';
describe('IDE store terminal mutations', () => {
let state;
diff --git a/spec/frontend/ide/stores/modules/terminal_sync/actions_spec.js b/spec/frontend/ide/stores/modules/terminal_sync/actions_spec.js
index 2ae7e8a8727..22b0615c6d0 100644
--- a/spec/frontend/ide/stores/modules/terminal_sync/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/terminal_sync/actions_spec.js
@@ -1,6 +1,6 @@
import testAction from 'helpers/vuex_action_helper';
-import * as actions from '~/ide/stores/modules/terminal_sync/actions';
import mirror, { canConnect, SERVICE_NAME } from '~/ide/lib/mirror';
+import * as actions from '~/ide/stores/modules/terminal_sync/actions';
import * as types from '~/ide/stores/modules/terminal_sync/mutation_types';
jest.mock('~/ide/lib/mirror');
diff --git a/spec/frontend/ide/stores/modules/terminal_sync/mutations_spec.js b/spec/frontend/ide/stores/modules/terminal_sync/mutations_spec.js
index ecf35d60e96..b7dbf93f4e6 100644
--- a/spec/frontend/ide/stores/modules/terminal_sync/mutations_spec.js
+++ b/spec/frontend/ide/stores/modules/terminal_sync/mutations_spec.js
@@ -1,6 +1,6 @@
-import createState from '~/ide/stores/modules/terminal_sync/state';
import * as types from '~/ide/stores/modules/terminal_sync/mutation_types';
import mutations from '~/ide/stores/modules/terminal_sync/mutations';
+import createState from '~/ide/stores/modules/terminal_sync/state';
const TEST_MESSAGE = 'lorem ipsum dolar sit';
diff --git a/spec/frontend/ide/stores/mutations/file_spec.js b/spec/frontend/ide/stores/mutations/file_spec.js
index 9bbdac0ae25..825d2a546cd 100644
--- a/spec/frontend/ide/stores/mutations/file_spec.js
+++ b/spec/frontend/ide/stores/mutations/file_spec.js
@@ -1,5 +1,5 @@
-import mutations from '~/ide/stores/mutations/file';
import { createStore } from '~/ide/stores';
+import mutations from '~/ide/stores/mutations/file';
import { file } from '../../helpers';
describe('IDE store file mutations', () => {
diff --git a/spec/frontend/ide/stores/plugins/terminal_spec.js b/spec/frontend/ide/stores/plugins/terminal_spec.js
index 948c2131fd8..d4cdad16ecb 100644
--- a/spec/frontend/ide/stores/plugins/terminal_spec.js
+++ b/spec/frontend/ide/stores/plugins/terminal_spec.js
@@ -2,8 +2,8 @@ import { createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { TEST_HOST } from 'helpers/test_constants';
import terminalModule from '~/ide/stores/modules/terminal';
-import createTerminalPlugin from '~/ide/stores/plugins/terminal';
import { SET_BRANCH_WORKING_REFERENCE } from '~/ide/stores/mutation_types';
+import createTerminalPlugin from '~/ide/stores/plugins/terminal';
const TEST_DATASET = {
eeWebTerminalSvgPath: `${TEST_HOST}/web/terminal/svg`,
diff --git a/spec/frontend/ide/stores/plugins/terminal_sync_spec.js b/spec/frontend/ide/stores/plugins/terminal_sync_spec.js
index 0e5f4184679..f12f80c1602 100644
--- a/spec/frontend/ide/stores/plugins/terminal_sync_spec.js
+++ b/spec/frontend/ide/stores/plugins/terminal_sync_spec.js
@@ -1,9 +1,9 @@
+import eventHub from '~/ide/eventhub';
+import { createStore } from '~/ide/stores';
+import { RUNNING, STOPPING } from '~/ide/stores/modules/terminal/constants';
+import { SET_SESSION_STATUS } from '~/ide/stores/modules/terminal/mutation_types';
import createTerminalPlugin from '~/ide/stores/plugins/terminal';
import createTerminalSyncPlugin from '~/ide/stores/plugins/terminal_sync';
-import { SET_SESSION_STATUS } from '~/ide/stores/modules/terminal/mutation_types';
-import { RUNNING, STOPPING } from '~/ide/stores/modules/terminal/constants';
-import { createStore } from '~/ide/stores';
-import eventHub from '~/ide/eventhub';
import { createTriggerUpdatePayload } from '../../helpers';
jest.mock('~/ide/lib/mirror');
diff --git a/spec/frontend/ide/stores/utils_spec.js b/spec/frontend/ide/stores/utils_spec.js
index b185013050e..46a0794b2e6 100644
--- a/spec/frontend/ide/stores/utils_spec.js
+++ b/spec/frontend/ide/stores/utils_spec.js
@@ -1,5 +1,5 @@
-import * as utils from '~/ide/stores/utils';
import { commitActionTypes } from '~/ide/constants';
+import * as utils from '~/ide/stores/utils';
import { file } from '../helpers';
describe('Multi-file store utils', () => {
diff --git a/spec/frontend/image_diff/image_badge_spec.js b/spec/frontend/image_diff/image_badge_spec.js
index a11b50ead47..8450b9b4694 100644
--- a/spec/frontend/image_diff/image_badge_spec.js
+++ b/spec/frontend/image_diff/image_badge_spec.js
@@ -1,5 +1,5 @@
-import ImageBadge from '~/image_diff/image_badge';
import imageDiffHelper from '~/image_diff/helpers/index';
+import ImageBadge from '~/image_diff/image_badge';
import * as mockData from './mock_data';
describe('ImageBadge', () => {
diff --git a/spec/frontend/image_diff/image_diff_spec.js b/spec/frontend/image_diff/image_diff_spec.js
index e14f8dc774f..16d19f45496 100644
--- a/spec/frontend/image_diff/image_diff_spec.js
+++ b/spec/frontend/image_diff/image_diff_spec.js
@@ -1,7 +1,7 @@
import { TEST_HOST } from 'helpers/test_constants';
+import imageDiffHelper from '~/image_diff/helpers/index';
import ImageDiff from '~/image_diff/image_diff';
import * as imageUtility from '~/lib/utils/image_utility';
-import imageDiffHelper from '~/image_diff/helpers/index';
import * as mockData from './mock_data';
describe('ImageDiff', () => {
diff --git a/spec/frontend/image_diff/init_discussion_tab_spec.js b/spec/frontend/image_diff/init_discussion_tab_spec.js
index f96d00230ee..5bc0c738944 100644
--- a/spec/frontend/image_diff/init_discussion_tab_spec.js
+++ b/spec/frontend/image_diff/init_discussion_tab_spec.js
@@ -1,5 +1,5 @@
-import initDiscussionTab from '~/image_diff/init_discussion_tab';
import initImageDiffHelper from '~/image_diff/helpers/init_image_diff';
+import initDiscussionTab from '~/image_diff/init_discussion_tab';
describe('initDiscussionTab', () => {
beforeEach(() => {
diff --git a/spec/frontend/image_diff/replaced_image_diff_spec.js b/spec/frontend/image_diff/replaced_image_diff_spec.js
index 16be4b82ad7..cc4a2530fc4 100644
--- a/spec/frontend/image_diff/replaced_image_diff_spec.js
+++ b/spec/frontend/image_diff/replaced_image_diff_spec.js
@@ -1,8 +1,8 @@
import { TEST_HOST } from 'helpers/test_constants';
-import ReplacedImageDiff from '~/image_diff/replaced_image_diff';
+import imageDiffHelper from '~/image_diff/helpers/index';
import ImageDiff from '~/image_diff/image_diff';
+import ReplacedImageDiff from '~/image_diff/replaced_image_diff';
import { viewTypes } from '~/image_diff/view_types';
-import imageDiffHelper from '~/image_diff/helpers/index';
describe('ReplacedImageDiff', () => {
let element;
diff --git a/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js
index ac8b73aeb49..cdef4b1ee62 100644
--- a/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js
@@ -1,8 +1,8 @@
-import { shallowMount } from '@vue/test-utils';
import { GlButton, GlLink, GlFormInput } from '@gitlab/ui';
-import Select2Select from '~/vue_shared/components/select2_select.vue';
-import ImportTableRow from '~/import_entities/import_groups/components/import_table_row.vue';
+import { shallowMount } from '@vue/test-utils';
import { STATUSES } from '~/import_entities/constants';
+import ImportTableRow from '~/import_entities/import_groups/components/import_table_row.vue';
+import Select2Select from '~/vue_shared/components/select2_select.vue';
import { availableNamespacesFixture } from '../graphql/fixtures';
const getFakeGroup = (status) => ({
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 cd184bb65cc..dd734782169 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,15 +1,15 @@
+import { GlEmptyState, GlLoadingIcon, GlSearchBoxByClick, GlSprintf } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
-import { GlLoadingIcon } from '@gitlab/ui';
-import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
-import ImportTableRow from '~/import_entities/import_groups/components/import_table_row.vue';
+import waitForPromises from 'helpers/wait_for_promises';
+import { STATUSES } from '~/import_entities/constants';
import ImportTable from '~/import_entities/import_groups/components/import_table.vue';
-import setTargetNamespaceMutation from '~/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql';
-import setNewNameMutation from '~/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql';
+import ImportTableRow from '~/import_entities/import_groups/components/import_table_row.vue';
import importGroupMutation from '~/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql';
-
-import { STATUSES } from '~/import_entities/constants';
+import setNewNameMutation from '~/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql';
+import setTargetNamespaceMutation from '~/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql';
+import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
import { availableNamespacesFixture, generateFakeEntry } from '../graphql/fixtures';
@@ -20,6 +20,9 @@ describe('import table', () => {
let wrapper;
let apolloProvider;
+ const FAKE_GROUP = generateFakeEntry({ id: 1, status: STATUSES.NONE });
+ const FAKE_PAGE_INFO = { page: 1, perPage: 20, total: 40, totalPages: 2 };
+
const createComponent = ({ bulkImportSourceGroups }) => {
apolloProvider = createMockApollo([], {
Query: {
@@ -34,6 +37,12 @@ describe('import table', () => {
});
wrapper = shallowMount(ImportTable, {
+ propsData: {
+ sourceUrl: 'https://demo.host',
+ },
+ stubs: {
+ GlSprintf,
+ },
localVue,
apolloProvider,
});
@@ -62,25 +71,50 @@ describe('import table', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
});
+ it('renders message about empty state when no groups are available for import', async () => {
+ createComponent({
+ bulkImportSourceGroups: () => ({
+ nodes: [],
+ pageInfo: FAKE_PAGE_INFO,
+ }),
+ });
+ await waitForPromises();
+
+ expect(wrapper.find(GlEmptyState).props().title).toBe('No groups available for import');
+ });
+
it('renders import row for each group in response', async () => {
const FAKE_GROUPS = [
generateFakeEntry({ id: 1, status: STATUSES.NONE }),
generateFakeEntry({ id: 2, status: STATUSES.FINISHED }),
];
createComponent({
- bulkImportSourceGroups: () => FAKE_GROUPS,
+ bulkImportSourceGroups: () => ({
+ nodes: FAKE_GROUPS,
+ pageInfo: FAKE_PAGE_INFO,
+ }),
});
await waitForPromises();
expect(wrapper.findAll(ImportTableRow)).toHaveLength(FAKE_GROUPS.length);
});
- describe('converts row events to mutation invocations', () => {
- const FAKE_GROUP = generateFakeEntry({ id: 1, status: STATUSES.NONE });
+ it('does not render status string when result list is empty', async () => {
+ createComponent({
+ bulkImportSourceGroups: jest.fn().mockResolvedValue({
+ nodes: [],
+ pageInfo: FAKE_PAGE_INFO,
+ }),
+ });
+ await waitForPromises();
+
+ expect(wrapper.text()).not.toContain('Showing 1-0');
+ });
+ describe('converts row events to mutation invocations', () => {
beforeEach(() => {
createComponent({
- bulkImportSourceGroups: () => [FAKE_GROUP],
+ bulkImportSourceGroups: () => ({ nodes: [FAKE_GROUP], pageInfo: FAKE_PAGE_INFO }),
});
return waitForPromises();
});
@@ -100,4 +134,115 @@ describe('import table', () => {
});
});
});
+
+ describe('pagination', () => {
+ const bulkImportSourceGroupsQueryMock = jest
+ .fn()
+ .mockResolvedValue({ nodes: [FAKE_GROUP], pageInfo: FAKE_PAGE_INFO });
+
+ beforeEach(() => {
+ createComponent({
+ bulkImportSourceGroups: bulkImportSourceGroupsQueryMock,
+ });
+ return waitForPromises();
+ });
+
+ it('correctly passes pagination info from query', () => {
+ expect(wrapper.find(PaginationLinks).props().pageInfo).toStrictEqual(FAKE_PAGE_INFO);
+ });
+
+ it('updates page when page change is requested', async () => {
+ const REQUESTED_PAGE = 2;
+ wrapper.find(PaginationLinks).props().change(REQUESTED_PAGE);
+
+ await waitForPromises();
+ expect(bulkImportSourceGroupsQueryMock).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({ page: REQUESTED_PAGE }),
+ expect.anything(),
+ expect.anything(),
+ );
+ });
+
+ it('updates status text when page is changed', async () => {
+ const REQUESTED_PAGE = 2;
+ bulkImportSourceGroupsQueryMock.mockResolvedValue({
+ nodes: [FAKE_GROUP],
+ pageInfo: {
+ page: 2,
+ total: 38,
+ perPage: 20,
+ totalPages: 2,
+ },
+ });
+ wrapper.find(PaginationLinks).props().change(REQUESTED_PAGE);
+ await waitForPromises();
+
+ expect(wrapper.text()).toContain('Showing 21-21 of 38');
+ });
+ });
+
+ describe('filters', () => {
+ const bulkImportSourceGroupsQueryMock = jest
+ .fn()
+ .mockResolvedValue({ nodes: [FAKE_GROUP], pageInfo: FAKE_PAGE_INFO });
+
+ beforeEach(() => {
+ createComponent({
+ bulkImportSourceGroups: bulkImportSourceGroupsQueryMock,
+ });
+ return waitForPromises();
+ });
+
+ const findFilterInput = () => wrapper.find(GlSearchBoxByClick);
+
+ it('properly passes filter to graphql query when search box is submitted', async () => {
+ createComponent({
+ bulkImportSourceGroups: bulkImportSourceGroupsQueryMock,
+ });
+ await waitForPromises();
+
+ const FILTER_VALUE = 'foo';
+ findFilterInput().vm.$emit('submit', FILTER_VALUE);
+ await waitForPromises();
+
+ expect(bulkImportSourceGroupsQueryMock).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({ filter: FILTER_VALUE }),
+ expect.anything(),
+ expect.anything(),
+ );
+ });
+
+ it('updates status string when search box is submitted', async () => {
+ createComponent({
+ bulkImportSourceGroups: bulkImportSourceGroupsQueryMock,
+ });
+ await waitForPromises();
+
+ const FILTER_VALUE = 'foo';
+ findFilterInput().vm.$emit('submit', FILTER_VALUE);
+ await waitForPromises();
+
+ expect(wrapper.text()).toContain('Showing 1-1 of 40 groups matching filter "foo"');
+ });
+
+ it('properly resets filter in graphql query when search box is cleared', async () => {
+ const FILTER_VALUE = 'foo';
+ findFilterInput().vm.$emit('submit', FILTER_VALUE);
+ await waitForPromises();
+
+ bulkImportSourceGroupsQueryMock.mockClear();
+ await apolloProvider.defaultClient.resetStore();
+ findFilterInput().vm.$emit('clear');
+ await waitForPromises();
+
+ expect(bulkImportSourceGroupsQueryMock).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({ filter: '' }),
+ expect.anything(),
+ expect.anything(),
+ );
+ });
+ });
});
diff --git a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
index 514ed411138..4d3d2c41bbe 100644
--- a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
+++ b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
@@ -1,20 +1,20 @@
-import MockAdapter from 'axios-mock-adapter';
import { InMemoryCache } from 'apollo-cache-inmemory';
+import MockAdapter from 'axios-mock-adapter';
import { createMockClient } from 'mock-apollo-client';
import waitForPromises from 'helpers/wait_for_promises';
-import axios from '~/lib/utils/axios_utils';
+import { STATUSES } from '~/import_entities/constants';
import {
clientTypenames,
createResolvers,
} from '~/import_entities/import_groups/graphql/client_factory';
+import importGroupMutation from '~/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql';
+import setNewNameMutation from '~/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql';
+import setTargetNamespaceMutation from '~/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql';
+import availableNamespacesQuery from '~/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql';
+import bulkImportSourceGroupsQuery from '~/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql';
import { StatusPoller } from '~/import_entities/import_groups/graphql/services/status_poller';
-import { STATUSES } from '~/import_entities/constants';
-import bulkImportSourceGroupsQuery from '~/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql';
-import availableNamespacesQuery from '~/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql';
-import setTargetNamespaceMutation from '~/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql';
-import setNewNameMutation from '~/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql';
-import importGroupMutation from '~/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql';
+import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
import { statusEndpointFixture, availableNamespacesFixture } from './fixtures';
@@ -28,6 +28,7 @@ const FAKE_ENDPOINTS = {
status: '/fake_status_url',
availableNamespaces: '/fake_available_namespaces',
createBulkImport: '/fake_create_bulk_import',
+ jobs: '/fake_jobs',
};
describe('Bulk import resolvers', () => {
@@ -79,33 +80,61 @@ describe('Bulk import resolvers', () => {
axiosMockAdapter
.onGet(FAKE_ENDPOINTS.availableNamespaces)
.reply(httpStatus.OK, availableNamespacesFixture);
-
- const response = await client.query({ query: bulkImportSourceGroupsQuery });
- results = response.data.bulkImportSourceGroups;
});
- it('mirrors REST endpoint response fields', () => {
- const MIRRORED_FIELDS = ['id', 'full_name', 'full_path', 'web_url'];
- expect(
- results.every((r, idx) =>
- MIRRORED_FIELDS.every(
- (field) => r[field] === statusEndpointFixture.importable_data[idx][field],
+ describe('when called', () => {
+ beforeEach(async () => {
+ const response = await client.query({ query: bulkImportSourceGroupsQuery });
+ results = response.data.bulkImportSourceGroups.nodes;
+ });
+
+ it('mirrors REST endpoint response fields', () => {
+ const MIRRORED_FIELDS = ['id', 'full_name', 'full_path', 'web_url'];
+ expect(
+ results.every((r, idx) =>
+ MIRRORED_FIELDS.every(
+ (field) => r[field] === statusEndpointFixture.importable_data[idx][field],
+ ),
),
- ),
- ).toBe(true);
- });
+ ).toBe(true);
+ });
- it('populates each result instance with status field default to none', () => {
- expect(results.every((r) => r.status === STATUSES.NONE)).toBe(true);
- });
+ it('populates each result instance with status field default to none', () => {
+ expect(results.every((r) => r.status === STATUSES.NONE)).toBe(true);
+ });
+
+ it('populates each result instance with import_target defaulted to first available namespace', () => {
+ expect(
+ results.every(
+ (r) => r.import_target.target_namespace === availableNamespacesFixture[0].full_path,
+ ),
+ ).toBe(true);
+ });
- it('populates each result instance with import_target defaulted to first available namespace', () => {
- expect(
- results.every(
- (r) => r.import_target.target_namespace === availableNamespacesFixture[0].full_path,
- ),
- ).toBe(true);
+ it('starts polling when request completes', async () => {
+ const [statusPoller] = StatusPoller.mock.instances;
+ expect(statusPoller.startPolling).toHaveBeenCalled();
+ });
});
+
+ it.each`
+ variable | queryParam | value
+ ${'filter'} | ${'filter'} | ${'demo'}
+ ${'perPage'} | ${'per_page'} | ${30}
+ ${'page'} | ${'page'} | ${3}
+ `(
+ 'properly passes GraphQL variable $variable as REST $queryParam query parameter',
+ async ({ variable, queryParam, value }) => {
+ await client.query({
+ query: bulkImportSourceGroupsQuery,
+ variables: { [variable]: value },
+ });
+ const restCall = axiosMockAdapter.history.get.find(
+ (q) => q.url === FAKE_ENDPOINTS.status,
+ );
+ expect(restCall.params[queryParam]).toBe(value);
+ },
+ );
});
});
@@ -117,20 +146,28 @@ describe('Bulk import resolvers', () => {
client.writeQuery({
query: bulkImportSourceGroupsQuery,
data: {
- bulkImportSourceGroups: [
- {
- __typename: clientTypenames.BulkImportSourceGroup,
- id: GROUP_ID,
- status: STATUSES.NONE,
- web_url: 'https://fake.host/1',
- full_path: 'fake_group_1',
- full_name: 'fake_name_1',
- import_target: {
- target_namespace: 'root',
- new_name: 'group1',
+ bulkImportSourceGroups: {
+ nodes: [
+ {
+ __typename: clientTypenames.BulkImportSourceGroup,
+ id: GROUP_ID,
+ status: STATUSES.NONE,
+ web_url: 'https://fake.host/1',
+ full_path: 'fake_group_1',
+ full_name: 'fake_name_1',
+ import_target: {
+ target_namespace: 'root',
+ new_name: 'group1',
+ },
},
+ ],
+ pageInfo: {
+ page: 1,
+ perPage: 20,
+ total: 37,
+ totalPages: 2,
},
- ],
+ },
},
});
@@ -140,7 +177,7 @@ describe('Bulk import resolvers', () => {
fetchPolicy: 'cache-only',
})
.subscribe(({ data }) => {
- results = data.bulkImportSourceGroups;
+ results = data.bulkImportSourceGroups.nodes;
});
});
@@ -174,7 +211,9 @@ describe('Bulk import resolvers', () => {
});
await waitForPromises();
- const { bulkImportSourceGroups: intermediateResults } = client.readQuery({
+ const {
+ bulkImportSourceGroups: { nodes: intermediateResults },
+ } = client.readQuery({
query: bulkImportSourceGroupsQuery,
});
@@ -182,7 +221,7 @@ describe('Bulk import resolvers', () => {
});
it('sets group status to STARTED when request completes', async () => {
- axiosMockAdapter.onPost(FAKE_ENDPOINTS.createBulkImport).reply(httpStatus.OK);
+ axiosMockAdapter.onPost(FAKE_ENDPOINTS.createBulkImport).reply(httpStatus.OK, { id: 1 });
await client.mutate({
mutation: importGroupMutation,
variables: { sourceGroupId: GROUP_ID },
@@ -191,16 +230,6 @@ describe('Bulk import resolvers', () => {
expect(results[0].status).toBe(STATUSES.STARTED);
});
- it('starts polling when request completes', async () => {
- axiosMockAdapter.onPost(FAKE_ENDPOINTS.createBulkImport).reply(httpStatus.OK);
- await client.mutate({
- mutation: importGroupMutation,
- variables: { sourceGroupId: GROUP_ID },
- });
- const [statusPoller] = StatusPoller.mock.instances;
- expect(statusPoller.startPolling).toHaveBeenCalled();
- });
-
it('resets status to NONE if request fails', async () => {
axiosMockAdapter
.onPost(FAKE_ENDPOINTS.createBulkImport)
diff --git a/spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js b/spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js
index 5940ea544ea..ca987ab3ab4 100644
--- a/spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js
+++ b/spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js
@@ -1,7 +1,7 @@
import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
-import { SourceGroupsManager } from '~/import_entities/import_groups/graphql/services/source_groups_manager';
-import ImportSourceGroupFragment from '~/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql';
import { clientTypenames } from '~/import_entities/import_groups/graphql/client_factory';
+import ImportSourceGroupFragment from '~/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql';
+import { SourceGroupsManager } from '~/import_entities/import_groups/graphql/services/source_groups_manager';
describe('SourceGroupsManager', () => {
let manager;
diff --git a/spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js b/spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js
index e7f1626f81d..a5fc4e18a02 100644
--- a/spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js
+++ b/spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js
@@ -1,215 +1,113 @@
-import { createMockClient } from 'mock-apollo-client';
-import { InMemoryCache } from 'apollo-cache-inmemory';
-import waitForPromises from 'helpers/wait_for_promises';
-
+import MockAdapter from 'axios-mock-adapter';
+import Visibility from 'visibilityjs';
import createFlash from '~/flash';
-import { StatusPoller } from '~/import_entities/import_groups/graphql/services/status_poller';
-import bulkImportSourceGroupsQuery from '~/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql';
import { STATUSES } from '~/import_entities/constants';
import { SourceGroupsManager } from '~/import_entities/import_groups/graphql/services/source_groups_manager';
-import { generateFakeEntry } from '../fixtures';
+import { StatusPoller } from '~/import_entities/import_groups/graphql/services/status_poller';
+import axios from '~/lib/utils/axios_utils';
+import Poll from '~/lib/utils/poll';
+jest.mock('visibilityjs');
jest.mock('~/flash');
+jest.mock('~/lib/utils/poll');
jest.mock('~/import_entities/import_groups/graphql/services/source_groups_manager', () => ({
SourceGroupsManager: jest.fn().mockImplementation(function mock() {
this.setImportStatus = jest.fn();
+ this.findByImportId = jest.fn();
}),
}));
-const TEST_POLL_INTERVAL = 1000;
+const FAKE_POLL_PATH = '/fake/poll/path';
+const CLIENT_MOCK = {};
describe('Bulk import status poller', () => {
let poller;
- let clientMock;
+ let mockAdapter;
- const listQueryCacheCalls = () =>
- clientMock.readQuery.mock.calls.filter((call) => call[0].query === bulkImportSourceGroupsQuery);
+ const getPollHistory = () => mockAdapter.history.get.filter((x) => x.url === FAKE_POLL_PATH);
beforeEach(() => {
- clientMock = createMockClient({
- cache: new InMemoryCache({
- fragmentMatcher: { match: () => true },
- }),
- });
-
- jest.spyOn(clientMock, 'readQuery');
-
- poller = new StatusPoller({
- client: clientMock,
- interval: TEST_POLL_INTERVAL,
- });
+ mockAdapter = new MockAdapter(axios);
+ mockAdapter.onGet(FAKE_POLL_PATH).reply(200, {});
+ poller = new StatusPoller({ client: CLIENT_MOCK, pollPath: FAKE_POLL_PATH });
});
- describe('general behavior', () => {
- beforeEach(() => {
- clientMock.cache.writeQuery({
- query: bulkImportSourceGroupsQuery,
- data: { bulkImportSourceGroups: [] },
- });
- });
-
- it('does not perform polling when constructed', () => {
- jest.runOnlyPendingTimers();
- expect(listQueryCacheCalls()).toHaveLength(0);
- });
-
- it('immediately start polling when requested', async () => {
- await poller.startPolling();
- expect(listQueryCacheCalls()).toHaveLength(1);
- });
-
- it('constantly polls when started', async () => {
- poller.startPolling();
- expect(listQueryCacheCalls()).toHaveLength(1);
-
- jest.advanceTimersByTime(TEST_POLL_INTERVAL);
- expect(listQueryCacheCalls()).toHaveLength(2);
-
- jest.advanceTimersByTime(TEST_POLL_INTERVAL);
- expect(listQueryCacheCalls()).toHaveLength(3);
- });
-
- it('does not start polling when requested multiple times', async () => {
- poller.startPolling();
- expect(listQueryCacheCalls()).toHaveLength(1);
-
- poller.startPolling();
- expect(listQueryCacheCalls()).toHaveLength(1);
- });
-
- it('stops polling when requested', async () => {
- poller.startPolling();
- expect(listQueryCacheCalls()).toHaveLength(1);
-
- poller.stopPolling();
- jest.runOnlyPendingTimers();
- expect(listQueryCacheCalls()).toHaveLength(1);
- });
-
- it('does not query server when list is empty', async () => {
- jest.spyOn(clientMock, 'query');
- poller.startPolling();
- expect(clientMock.query).not.toHaveBeenCalled();
- });
+ it('creates source group manager with proper client', () => {
+ expect(SourceGroupsManager.mock.calls).toHaveLength(1);
+ const [[{ client }]] = SourceGroupsManager.mock.calls;
+ expect(client).toBe(CLIENT_MOCK);
});
- it('does not query server when no groups have STARTED status', async () => {
- clientMock.cache.writeQuery({
- query: bulkImportSourceGroupsQuery,
- data: {
- bulkImportSourceGroups: [STATUSES.NONE, STATUSES.FINISHED].map((status, idx) =>
- generateFakeEntry({ status, id: idx }),
- ),
- },
- });
-
- jest.spyOn(clientMock, 'query');
+ it('creates poller with proper config', () => {
+ expect(Poll.mock.calls).toHaveLength(1);
+ const [[pollConfig]] = Poll.mock.calls;
+ expect(typeof pollConfig.method).toBe('string');
+
+ const pollOperation = pollConfig.resource[pollConfig.method];
+ expect(typeof pollOperation).toBe('function');
+ });
+
+ it('invokes axios when polling is performed', async () => {
+ const [[pollConfig]] = Poll.mock.calls;
+ const pollOperation = pollConfig.resource[pollConfig.method];
+ expect(getPollHistory()).toHaveLength(0);
+
+ pollOperation();
+ await axios.waitForAll();
+
+ expect(getPollHistory()).toHaveLength(1);
+ });
+
+ it('subscribes to visibility changes', () => {
+ expect(Visibility.change).toHaveBeenCalled();
+ });
+
+ it.each`
+ isHidden | action
+ ${true} | ${'stop'}
+ ${false} | ${'restart'}
+ `('$action polling when hidden is $isHidden', ({ action, isHidden }) => {
+ const [pollInstance] = Poll.mock.instances;
+ const [[changeHandler]] = Visibility.change.mock.calls;
+ Visibility.hidden.mockReturnValue(isHidden);
+ expect(pollInstance[action]).not.toHaveBeenCalled();
+
+ changeHandler();
+
+ expect(pollInstance[action]).toHaveBeenCalled();
+ });
+
+ it('does not perform polling when constructed', async () => {
+ await axios.waitForAll();
+
+ expect(getPollHistory()).toHaveLength(0);
+ });
+
+ it('immediately start polling when requested', async () => {
+ const [pollInstance] = Poll.mock.instances;
+
poller.startPolling();
- expect(clientMock.query).not.toHaveBeenCalled();
+
+ expect(pollInstance.makeRequest).toHaveBeenCalled();
+ });
+
+ it('when error occurs shows flash with error', () => {
+ const [[pollConfig]] = Poll.mock.calls;
+ pollConfig.errorCallback();
+ expect(createFlash).toHaveBeenCalled();
});
- describe('when there are groups which have STARTED status', () => {
- const TARGET_NAMESPACE = 'root';
-
- const STARTED_GROUP_1 = {
- status: STATUSES.STARTED,
- id: 'started1',
- import_target: {
- target_namespace: TARGET_NAMESPACE,
- new_name: 'group1',
- },
- };
-
- const STARTED_GROUP_2 = {
- status: STATUSES.STARTED,
- id: 'started2',
- import_target: {
- target_namespace: TARGET_NAMESPACE,
- new_name: 'group2',
- },
- };
-
- const NOT_STARTED_GROUP = {
- status: STATUSES.NONE,
- id: 'not_started',
- import_target: {
- target_namespace: TARGET_NAMESPACE,
- new_name: 'group3',
- },
- };
-
- it('query server only for groups with STATUSES.STARTED', async () => {
- clientMock.cache.writeQuery({
- query: bulkImportSourceGroupsQuery,
- data: {
- bulkImportSourceGroups: [
- STARTED_GROUP_1,
- NOT_STARTED_GROUP,
- STARTED_GROUP_2,
- ].map((group) => generateFakeEntry(group)),
- },
- });
-
- clientMock.query = jest.fn().mockResolvedValue({ data: {} });
- poller.startPolling();
-
- expect(clientMock.query).toHaveBeenCalledTimes(1);
- await waitForPromises();
- const [[doc]] = clientMock.query.mock.calls;
- const { selections } = doc.query.definitions[0].selectionSet;
- expect(selections.every((field) => field.name.value === 'group')).toBeTruthy();
- expect(selections).toHaveLength(2);
- expect(selections.map((sel) => sel.arguments[0].value.value)).toStrictEqual([
- `${TARGET_NAMESPACE}/${STARTED_GROUP_1.import_target.new_name}`,
- `${TARGET_NAMESPACE}/${STARTED_GROUP_2.import_target.new_name}`,
- ]);
- });
-
- it('updates statuses only for groups in response', async () => {
- clientMock.cache.writeQuery({
- query: bulkImportSourceGroupsQuery,
- data: {
- bulkImportSourceGroups: [STARTED_GROUP_1, STARTED_GROUP_2].map((group) =>
- generateFakeEntry(group),
- ),
- },
- });
-
- clientMock.query = jest.fn().mockResolvedValue({ data: { group0: {} } });
- poller.startPolling();
- await waitForPromises();
- const [managerInstance] = SourceGroupsManager.mock.instances;
- expect(managerInstance.setImportStatus).toHaveBeenCalledTimes(1);
- expect(managerInstance.setImportStatus).toHaveBeenCalledWith(
- expect.objectContaining({ id: STARTED_GROUP_1.id }),
- STATUSES.FINISHED,
- );
- });
-
- describe('when error occurs', () => {
- beforeEach(() => {
- clientMock.cache.writeQuery({
- query: bulkImportSourceGroupsQuery,
- data: {
- bulkImportSourceGroups: [STARTED_GROUP_1, STARTED_GROUP_2].map((group) =>
- generateFakeEntry(group),
- ),
- },
- });
-
- clientMock.query = jest.fn().mockRejectedValue(new Error('dummy error'));
- poller.startPolling();
- return waitForPromises();
- });
-
- it('reports an error', () => {
- expect(createFlash).toHaveBeenCalled();
- });
-
- it('continues polling', async () => {
- jest.advanceTimersByTime(TEST_POLL_INTERVAL);
- expect(listQueryCacheCalls()).toHaveLength(2);
- });
- });
+ it('when success response arrives updates relevant group status', () => {
+ const FAKE_ID = 5;
+ const [[pollConfig]] = Poll.mock.calls;
+ const [managerInstance] = SourceGroupsManager.mock.instances;
+ managerInstance.findByImportId.mockReturnValue({ id: FAKE_ID });
+
+ pollConfig.successCallback({ data: [{ id: FAKE_ID, status_name: STATUSES.FINISHED }] });
+
+ expect(managerInstance.setImportStatus).toHaveBeenCalledWith(
+ expect.objectContaining({ id: FAKE_ID }),
+ STATUSES.FINISHED,
+ );
});
});
diff --git a/spec/frontend/import_entities/import_projects/components/bitbucket_status_table_spec.js b/spec/frontend/import_entities/import_projects/components/bitbucket_status_table_spec.js
index 8f8c01a8b81..ea88c361f7b 100644
--- a/spec/frontend/import_entities/import_projects/components/bitbucket_status_table_spec.js
+++ b/spec/frontend/import_entities/import_projects/components/bitbucket_status_table_spec.js
@@ -1,7 +1,7 @@
-import { nextTick } from 'vue';
+import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
-import { GlAlert } from '@gitlab/ui';
import BitbucketStatusTable from '~/import_entities/import_projects/components/bitbucket_status_table.vue';
import ImportProjectsTable from '~/import_entities/import_projects/components/import_projects_table.vue';
diff --git a/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js b/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js
index 27f642d15c8..d9f4168f1a5 100644
--- a/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js
+++ b/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js
@@ -1,18 +1,20 @@
+import { GlLoadingIcon, GlButton, GlIntersectionObserver, GlFormInput } from '@gitlab/ui';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import Vuex from 'vuex';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
-import { GlLoadingIcon, GlButton, GlIntersectionObserver } from '@gitlab/ui';
-import state from '~/import_entities/import_projects/store/state';
-import * as getters from '~/import_entities/import_projects/store/getters';
import { STATUSES } from '~/import_entities/constants';
import ImportProjectsTable from '~/import_entities/import_projects/components/import_projects_table.vue';
import ProviderRepoTableRow from '~/import_entities/import_projects/components/provider_repo_table_row.vue';
+import * as getters from '~/import_entities/import_projects/store/getters';
+import state from '~/import_entities/import_projects/store/state';
describe('ImportProjectsTable', () => {
let wrapper;
const findFilterField = () =>
- wrapper.find('input[data-qa-selector="githubish_import_filter_field"]');
+ wrapper
+ .findAllComponents(GlFormInput)
+ .wrappers.find((w) => w.attributes('placeholder') === 'Filter your repositories by name');
const providerTitle = 'THE PROVIDER';
const providerRepo = {
@@ -205,7 +207,7 @@ describe('ImportProjectsTable', () => {
it('does not render filtering input field when filterable is false', () => {
createComponent({ filterable: false });
- expect(findFilterField().exists()).toBe(false);
+ expect(findFilterField()).toBeUndefined();
});
describe('when paginatable is set to true', () => {
diff --git a/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
index 2ed11ae277e..e15389be53a 100644
--- a/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
+++ b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
@@ -1,10 +1,10 @@
+import { GlBadge, GlButton } from '@gitlab/ui';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import Vuex from 'vuex';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
-import { GlBadge } from '@gitlab/ui';
-import ProviderRepoTableRow from '~/import_entities/import_projects/components/provider_repo_table_row.vue';
-import ImportStatus from '~/import_entities/components/import_status.vue';
import { STATUSES } from '~/import_entities//constants';
+import ImportStatus from '~/import_entities/components/import_status.vue';
+import ProviderRepoTableRow from '~/import_entities/import_projects/components/provider_repo_table_row.vue';
import Select2Select from '~/vue_shared/components/select2_select.vue';
describe('ProviderRepoTableRow', () => {
@@ -34,7 +34,7 @@ describe('ProviderRepoTableRow', () => {
}
const findImportButton = () => {
- const buttons = wrapper.findAll('button').filter((node) => node.text() === 'Import');
+ const buttons = wrapper.findAllComponents(GlButton).filter((node) => node.text() === 'Import');
return buttons.length ? buttons.at(0) : buttons;
};
@@ -91,7 +91,7 @@ describe('ProviderRepoTableRow', () => {
});
it('imports repo when clicking import button', async () => {
- findImportButton().trigger('click');
+ findImportButton().vm.$emit('click');
await nextTick();
diff --git a/spec/frontend/import_entities/import_projects/store/actions_spec.js b/spec/frontend/import_entities/import_projects/store/actions_spec.js
index bd731dc3929..9bff77cd34a 100644
--- a/spec/frontend/import_entities/import_projects/store/actions_spec.js
+++ b/spec/frontend/import_entities/import_projects/store/actions_spec.js
@@ -1,9 +1,10 @@
import MockAdapter from 'axios-mock-adapter';
-import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'helpers/test_constants';
+import testAction from 'helpers/vuex_action_helper';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import axios from '~/lib/utils/axios_utils';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { STATUSES } from '~/import_entities/constants';
+import actionsFactory from '~/import_entities/import_projects/store/actions';
+import { getImportTarget } from '~/import_entities/import_projects/store/getters';
import {
REQUEST_REPOS,
RECEIVE_REPOS_SUCCESS,
@@ -18,10 +19,9 @@ import {
SET_PAGE,
SET_FILTER,
} from '~/import_entities/import_projects/store/mutation_types';
-import actionsFactory from '~/import_entities/import_projects/store/actions';
-import { getImportTarget } from '~/import_entities/import_projects/store/getters';
import state from '~/import_entities/import_projects/store/state';
-import { STATUSES } from '~/import_entities/constants';
+import axios from '~/lib/utils/axios_utils';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
jest.mock('~/flash');
diff --git a/spec/frontend/import_entities/import_projects/store/getters_spec.js b/spec/frontend/import_entities/import_projects/store/getters_spec.js
index f0ccffc19f2..55826b20ca3 100644
--- a/spec/frontend/import_entities/import_projects/store/getters_spec.js
+++ b/spec/frontend/import_entities/import_projects/store/getters_spec.js
@@ -1,3 +1,4 @@
+import { STATUSES } from '~/import_entities/constants';
import {
isLoading,
isImportingAnyRepo,
@@ -6,7 +7,6 @@ import {
importAllCount,
getImportTarget,
} from '~/import_entities/import_projects/store/getters';
-import { STATUSES } from '~/import_entities/constants';
import state from '~/import_entities/import_projects/store/state';
const IMPORTED_REPO = {
diff --git a/spec/frontend/import_entities/import_projects/store/mutations_spec.js b/spec/frontend/import_entities/import_projects/store/mutations_spec.js
index 8b7ddffe6f4..e062d889325 100644
--- a/spec/frontend/import_entities/import_projects/store/mutations_spec.js
+++ b/spec/frontend/import_entities/import_projects/store/mutations_spec.js
@@ -1,7 +1,7 @@
+import { STATUSES } from '~/import_entities/constants';
import * as types from '~/import_entities/import_projects/store/mutation_types';
import mutations from '~/import_entities/import_projects/store/mutations';
import getInitialState from '~/import_entities/import_projects/store/state';
-import { STATUSES } from '~/import_entities/constants';
describe('import_projects store mutations', () => {
let state;
diff --git a/spec/frontend/import_entities/import_projects/utils_spec.js b/spec/frontend/import_entities/import_projects/utils_spec.js
index 7d9c4b7137e..d705f0acbfe 100644
--- a/spec/frontend/import_entities/import_projects/utils_spec.js
+++ b/spec/frontend/import_entities/import_projects/utils_spec.js
@@ -1,9 +1,9 @@
+import { STATUSES } from '~/import_entities/constants';
import {
isProjectImportable,
isIncompatible,
getImportStatus,
} from '~/import_entities/import_projects/utils';
-import { STATUSES } from '~/import_entities/constants';
describe('import_projects utils', () => {
const COMPATIBLE_PROJECT = {
diff --git a/spec/frontend/incidents/components/incidents_list_spec.js b/spec/frontend/incidents/components/incidents_list_spec.js
index 8212776742c..df681658081 100644
--- a/spec/frontend/incidents/components/incidents_list_spec.js
+++ b/spec/frontend/incidents/components/incidents_list_spec.js
@@ -1,10 +1,6 @@
-import { mount } from '@vue/test-utils';
import { GlAlert, GlLoadingIcon, GlTable, GlAvatar, GlEmptyState } from '@gitlab/ui';
-import Tracking from '~/tracking';
-import { visitUrl, joinPaths, mergeUrlParams } from '~/lib/utils/url_utility';
+import { mount } from '@vue/test-utils';
import IncidentsList from '~/incidents/components/incidents_list.vue';
-import SeverityToken from '~/sidebar/components/severity/severity.vue';
-import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import {
I18N,
TH_CREATED_AT_TEST_ID,
@@ -14,6 +10,10 @@ import {
trackIncidentCreateNewOptions,
trackIncidentListViewsOptions,
} from '~/incidents/constants';
+import { visitUrl, joinPaths, mergeUrlParams } from '~/lib/utils/url_utility';
+import SeverityToken from '~/sidebar/components/severity/severity.vue';
+import Tracking from '~/tracking';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import mockIncidents from '../mocks/incidents.json';
jest.mock('~/lib/utils/url_utility', () => ({
diff --git a/spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap b/spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap
index 2b3c803be08..4398d568501 100644
--- a/spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap
+++ b/spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap
@@ -9,9 +9,7 @@ exports[`IncidentsSettingTabs should render the component 1`] = `
<div
class="settings-header"
>
- <h4
- class="gl-my-3! gl-py-1"
- >
+ <h4>
Incidents
diff --git a/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js b/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js
index 5010fc0bb5c..5476e895c68 100644
--- a/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js
+++ b/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js
@@ -1,9 +1,9 @@
import AxiosMockAdapter from 'axios-mock-adapter';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+import { ERROR_MSG } from '~/incidents_settings/constants';
+import IncidentsSettingsService from '~/incidents_settings/incidents_settings_service';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
-import IncidentsSettingsService from '~/incidents_settings/incidents_settings_service';
-import { ERROR_MSG } from '~/incidents_settings/constants';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
jest.mock('~/flash');
diff --git a/spec/frontend/incidents_settings/components/incidents_settings_tabs_spec.js b/spec/frontend/incidents_settings/components/incidents_settings_tabs_spec.js
index c80d9ad2e5b..ff40f1fa008 100644
--- a/spec/frontend/incidents_settings/components/incidents_settings_tabs_spec.js
+++ b/spec/frontend/incidents_settings/components/incidents_settings_tabs_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlTab } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import IncidentsSettingTabs from '~/incidents_settings/components/incidents_settings_tabs.vue';
describe('IncidentsSettingTabs', () => {
diff --git a/spec/frontend/incidents_settings/components/pagerduty_form_spec.js b/spec/frontend/incidents_settings/components/pagerduty_form_spec.js
index 50d0de8a753..2ffd1292ddc 100644
--- a/spec/frontend/incidents_settings/components/pagerduty_form_spec.js
+++ b/spec/frontend/incidents_settings/components/pagerduty_form_spec.js
@@ -1,6 +1,6 @@
+import { GlAlert, GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
-import { GlAlert, GlModal } from '@gitlab/ui';
import PagerDutySettingsForm from '~/incidents_settings/components/pagerduty_form.vue';
describe('Alert integration settings form', () => {
diff --git a/spec/frontend/integrations/edit/components/active_checkbox_spec.js b/spec/frontend/integrations/edit/components/active_checkbox_spec.js
index 38bcb1e0aab..76fd6dd3a48 100644
--- a/spec/frontend/integrations/edit/components/active_checkbox_spec.js
+++ b/spec/frontend/integrations/edit/components/active_checkbox_spec.js
@@ -1,8 +1,7 @@
-import { mount } from '@vue/test-utils';
import { GlFormCheckbox } from '@gitlab/ui';
-import { createStore } from '~/integrations/edit/store';
-
+import { mount } from '@vue/test-utils';
import ActiveCheckbox from '~/integrations/edit/components/active_checkbox.vue';
+import { createStore } from '~/integrations/edit/store';
describe('ActiveCheckbox', () => {
let wrapper;
diff --git a/spec/frontend/integrations/edit/components/confirmation_modal_spec.js b/spec/frontend/integrations/edit/components/confirmation_modal_spec.js
index b570ab4e844..1c126f60c37 100644
--- a/spec/frontend/integrations/edit/components/confirmation_modal_spec.js
+++ b/spec/frontend/integrations/edit/components/confirmation_modal_spec.js
@@ -1,8 +1,7 @@
-import { shallowMount } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui';
-import { createStore } from '~/integrations/edit/store';
-
+import { shallowMount } from '@vue/test-utils';
import ConfirmationModal from '~/integrations/edit/components/confirmation_modal.vue';
+import { createStore } from '~/integrations/edit/store';
describe('ConfirmationModal', () => {
let wrapper;
diff --git a/spec/frontend/integrations/edit/components/dynamic_field_spec.js b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
index ecd03103992..aaca9fc4e62 100644
--- a/spec/frontend/integrations/edit/components/dynamic_field_spec.js
+++ b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
@@ -1,5 +1,5 @@
-import { mount } from '@vue/test-utils';
import { GlFormGroup, GlFormCheckbox, GlFormInput, GlFormSelect, GlFormTextarea } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import DynamicField from '~/integrations/edit/components/dynamic_field.vue';
describe('DynamicField', () => {
diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js
index 97e77ac87ab..df855674804 100644
--- a/spec/frontend/integrations/edit/components/integration_form_spec.js
+++ b/spec/frontend/integrations/edit/components/integration_form_spec.js
@@ -1,38 +1,47 @@
import { shallowMount } from '@vue/test-utils';
+import { setHTMLFixture } from 'helpers/fixtures';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { mockIntegrationProps } from 'jest/integrations/edit/mock_data';
-import { createStore } from '~/integrations/edit/store';
-import IntegrationForm from '~/integrations/edit/components/integration_form.vue';
-import OverrideDropdown from '~/integrations/edit/components/override_dropdown.vue';
import ActiveCheckbox from '~/integrations/edit/components/active_checkbox.vue';
import ConfirmationModal from '~/integrations/edit/components/confirmation_modal.vue';
-import ResetConfirmationModal from '~/integrations/edit/components/reset_confirmation_modal.vue';
-import JiraTriggerFields from '~/integrations/edit/components/jira_trigger_fields.vue';
+import DynamicField from '~/integrations/edit/components/dynamic_field.vue';
+import IntegrationForm from '~/integrations/edit/components/integration_form.vue';
import JiraIssuesFields from '~/integrations/edit/components/jira_issues_fields.vue';
+import JiraTriggerFields from '~/integrations/edit/components/jira_trigger_fields.vue';
+import OverrideDropdown from '~/integrations/edit/components/override_dropdown.vue';
+import ResetConfirmationModal from '~/integrations/edit/components/reset_confirmation_modal.vue';
import TriggerFields from '~/integrations/edit/components/trigger_fields.vue';
-import DynamicField from '~/integrations/edit/components/dynamic_field.vue';
import { integrationLevels } from '~/integrations/edit/constants';
+import { createStore } from '~/integrations/edit/store';
describe('IntegrationForm', () => {
let wrapper;
- const createComponent = (customStateProps = {}, featureFlags = {}, initialState = {}) => {
- wrapper = shallowMount(IntegrationForm, {
- propsData: {},
- store: createStore({
- customState: { ...mockIntegrationProps, ...customStateProps },
- ...initialState,
+ const createComponent = ({
+ customStateProps = {},
+ featureFlags = {},
+ initialState = {},
+ props = {},
+ } = {}) => {
+ wrapper = extendedWrapper(
+ shallowMount(IntegrationForm, {
+ propsData: { ...props },
+ store: createStore({
+ customState: { ...mockIntegrationProps, ...customStateProps },
+ ...initialState,
+ }),
+ stubs: {
+ OverrideDropdown,
+ ActiveCheckbox,
+ ConfirmationModal,
+ JiraTriggerFields,
+ TriggerFields,
+ },
+ provide: {
+ glFeatures: featureFlags,
+ },
}),
- stubs: {
- OverrideDropdown,
- ActiveCheckbox,
- ConfirmationModal,
- JiraTriggerFields,
- TriggerFields,
- },
- provide: {
- glFeatures: featureFlags,
- },
- });
+ );
};
afterEach(() => {
@@ -63,7 +72,9 @@ describe('IntegrationForm', () => {
describe('showActive is false', () => {
it('does not render ActiveCheckbox', () => {
createComponent({
- showActive: false,
+ customStateProps: {
+ showActive: false,
+ },
});
expect(findActiveCheckbox().exists()).toBe(false);
@@ -73,7 +84,9 @@ describe('IntegrationForm', () => {
describe('integrationLevel is instance', () => {
it('renders ConfirmationModal', () => {
createComponent({
- integrationLevel: integrationLevels.INSTANCE,
+ customStateProps: {
+ integrationLevel: integrationLevels.INSTANCE,
+ },
});
expect(findConfirmationModal().exists()).toBe(true);
@@ -82,7 +95,9 @@ describe('IntegrationForm', () => {
describe('resetPath is empty', () => {
it('does not render ResetConfirmationModal and button', () => {
createComponent({
- integrationLevel: integrationLevels.INSTANCE,
+ customStateProps: {
+ integrationLevel: integrationLevels.INSTANCE,
+ },
});
expect(findResetButton().exists()).toBe(false);
@@ -93,8 +108,10 @@ describe('IntegrationForm', () => {
describe('resetPath is present', () => {
it('renders ResetConfirmationModal and button', () => {
createComponent({
- integrationLevel: integrationLevels.INSTANCE,
- resetPath: 'resetPath',
+ customStateProps: {
+ integrationLevel: integrationLevels.INSTANCE,
+ resetPath: 'resetPath',
+ },
});
expect(findResetButton().exists()).toBe(true);
@@ -106,7 +123,9 @@ describe('IntegrationForm', () => {
describe('integrationLevel is group', () => {
it('renders ConfirmationModal', () => {
createComponent({
- integrationLevel: integrationLevels.GROUP,
+ customStateProps: {
+ integrationLevel: integrationLevels.GROUP,
+ },
});
expect(findConfirmationModal().exists()).toBe(true);
@@ -115,7 +134,9 @@ describe('IntegrationForm', () => {
describe('resetPath is empty', () => {
it('does not render ResetConfirmationModal and button', () => {
createComponent({
- integrationLevel: integrationLevels.GROUP,
+ customStateProps: {
+ integrationLevel: integrationLevels.GROUP,
+ },
});
expect(findResetButton().exists()).toBe(false);
@@ -126,8 +147,10 @@ describe('IntegrationForm', () => {
describe('resetPath is present', () => {
it('renders ResetConfirmationModal and button', () => {
createComponent({
- integrationLevel: integrationLevels.GROUP,
- resetPath: 'resetPath',
+ customStateProps: {
+ integrationLevel: integrationLevels.GROUP,
+ resetPath: 'resetPath',
+ },
});
expect(findResetButton().exists()).toBe(true);
@@ -139,7 +162,9 @@ describe('IntegrationForm', () => {
describe('integrationLevel is project', () => {
it('does not render ConfirmationModal', () => {
createComponent({
- integrationLevel: 'project',
+ customStateProps: {
+ integrationLevel: 'project',
+ },
});
expect(findConfirmationModal().exists()).toBe(false);
@@ -147,8 +172,10 @@ describe('IntegrationForm', () => {
it('does not render ResetConfirmationModal and button', () => {
createComponent({
- integrationLevel: 'project',
- resetPath: 'resetPath',
+ customStateProps: {
+ integrationLevel: 'project',
+ resetPath: 'resetPath',
+ },
});
expect(findResetButton().exists()).toBe(false);
@@ -158,7 +185,9 @@ describe('IntegrationForm', () => {
describe('type is "slack"', () => {
beforeEach(() => {
- createComponent({ type: 'slack' });
+ createComponent({
+ customStateProps: { type: 'slack' },
+ });
});
it('does not render JiraTriggerFields', () => {
@@ -172,14 +201,19 @@ describe('IntegrationForm', () => {
describe('type is "jira"', () => {
it('renders JiraTriggerFields', () => {
- createComponent({ type: 'jira' });
+ createComponent({
+ customStateProps: { type: 'jira' },
+ });
expect(findJiraTriggerFields().exists()).toBe(true);
});
describe('featureFlag jiraIssuesIntegration is false', () => {
it('does not render JiraIssuesFields', () => {
- createComponent({ type: 'jira' }, { jiraIssuesIntegration: false });
+ createComponent({
+ customStateProps: { type: 'jira' },
+ featureFlags: { jiraIssuesIntegration: false },
+ });
expect(findJiraIssuesFields().exists()).toBe(false);
});
@@ -187,8 +221,10 @@ describe('IntegrationForm', () => {
describe('featureFlag jiraIssuesIntegration is true', () => {
it('renders JiraIssuesFields', () => {
- createComponent({ type: 'jira' }, { jiraIssuesIntegration: true });
-
+ createComponent({
+ customStateProps: { type: 'jira' },
+ featureFlags: { jiraIssuesIntegration: true },
+ });
expect(findJiraIssuesFields().exists()).toBe(true);
});
});
@@ -200,8 +236,10 @@ describe('IntegrationForm', () => {
const type = 'slack';
createComponent({
- triggerEvents: events,
- type,
+ customStateProps: {
+ triggerEvents: events,
+ type,
+ },
});
expect(findTriggerFields().exists()).toBe(true);
@@ -218,7 +256,9 @@ describe('IntegrationForm', () => {
];
createComponent({
- fields,
+ customStateProps: {
+ fields,
+ },
});
const dynamicFields = wrapper.findAll(DynamicField);
@@ -232,13 +272,11 @@ describe('IntegrationForm', () => {
describe('defaultState state is null', () => {
it('does not render OverrideDropdown', () => {
- createComponent(
- {},
- {},
- {
+ createComponent({
+ initialState: {
defaultState: null,
},
- );
+ });
expect(findOverrideDropdown().exists()).toBe(false);
});
@@ -246,18 +284,43 @@ describe('IntegrationForm', () => {
describe('defaultState state is an object', () => {
it('renders OverrideDropdown', () => {
- createComponent(
- {},
- {},
- {
+ createComponent({
+ initialState: {
defaultState: {
...mockIntegrationProps,
},
},
- );
+ });
expect(findOverrideDropdown().exists()).toBe(true);
});
});
+
+ describe('with `helpHtml` prop', () => {
+ const mockTestId = 'jest-help-html-test';
+
+ setHTMLFixture(`
+ <div data-testid="${mockTestId}">
+ <svg class="gl-icon">
+ <use></use>
+ </svg>
+ </div>
+ `);
+
+ it('renders `helpHtml`', async () => {
+ const mockHelpHtml = document.querySelector(`[data-testid="${mockTestId}"]`);
+
+ createComponent({
+ props: {
+ helpHtml: mockHelpHtml.outerHTML,
+ },
+ });
+
+ const helpHtml = wrapper.findByTestId(mockTestId);
+
+ expect(helpHtml.isVisible()).toBe(true);
+ expect(helpHtml.find('svg').isVisible()).toBe(true);
+ });
+ });
});
});
diff --git a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
index eaeed2703d1..3938e7c7c22 100644
--- a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
+++ b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
@@ -1,20 +1,23 @@
-import { mount } from '@vue/test-utils';
-
import { GlFormCheckbox, GlFormInput } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import JiraIssuesFields from '~/integrations/edit/components/jira_issues_fields.vue';
+import eventHub from '~/integrations/edit/event_hub';
describe('JiraIssuesFields', () => {
let wrapper;
const defaultProps = {
- showJiraIssuesIntegration: true,
editProjectPath: '/edit',
+ showJiraIssuesIntegration: true,
+ showJiraVulnerabilitiesIntegration: true,
};
- const createComponent = (props) => {
+ const createComponent = ({ props, ...options } = {}) => {
wrapper = mount(JiraIssuesFields, {
propsData: { ...defaultProps, ...props },
+ stubs: ['jira-issue-creation-vulnerabilities'],
+ ...options,
});
};
@@ -28,11 +31,14 @@ describe('JiraIssuesFields', () => {
const findEnableCheckbox = () => wrapper.find(GlFormCheckbox);
const findProjectKey = () => wrapper.find(GlFormInput);
const expectedBannerText = 'This is a Premium feature';
+ const findJiraForVulnerabilities = () => wrapper.find('[data-testid="jira-for-vulnerabilities"]');
+ const setEnableCheckbox = async (isEnabled = true) =>
+ findEnableCheckbox().vm.$emit('input', isEnabled);
describe('template', () => {
describe('upgrade banner for non-Premium user', () => {
beforeEach(() => {
- createComponent({ initialProjectKey: '', showJiraIssuesIntegration: false });
+ createComponent({ props: { initialProjectKey: '', showJiraIssuesIntegration: false } });
});
it('shows upgrade banner', () => {
@@ -47,7 +53,7 @@ describe('JiraIssuesFields', () => {
describe('Enable Jira issues checkbox', () => {
beforeEach(() => {
- createComponent({ initialProjectKey: '' });
+ createComponent({ props: { initialProjectKey: '' } });
});
it('does not show upgrade banner', () => {
@@ -69,20 +75,16 @@ describe('JiraIssuesFields', () => {
});
describe('on enable issues', () => {
- it('enables project_key input', () => {
- findEnableCheckbox().vm.$emit('input', true);
+ it('enables project_key input', async () => {
+ await setEnableCheckbox(true);
- return wrapper.vm.$nextTick().then(() => {
- expect(findProjectKey().attributes('disabled')).toBeUndefined();
- });
+ expect(findProjectKey().attributes('disabled')).toBeUndefined();
});
- it('requires project_key input', () => {
- findEnableCheckbox().vm.$emit('input', true);
+ it('requires project_key input', async () => {
+ await setEnableCheckbox(true);
- return wrapper.vm.$nextTick().then(() => {
- expect(findProjectKey().attributes('required')).toBe('required');
- });
+ expect(findProjectKey().attributes('required')).toBe('required');
});
});
});
@@ -103,10 +105,60 @@ describe('JiraIssuesFields', () => {
});
it('does not contain warning when GitLab issues is disabled', () => {
- createComponent({ gitlabIssuesEnabled: false });
+ createComponent({ props: { gitlabIssuesEnabled: false } });
expect(wrapper.text()).not.toContain(expectedText);
});
});
+
+ describe('Vulnerabilities creation', () => {
+ beforeEach(() => {
+ createComponent({ provide: { glFeatures: { jiraForVulnerabilities: true } } });
+ });
+
+ it.each([true, false])(
+ 'shows the jira-vulnerabilities component correctly when jira issues enables is set to "%s"',
+ async (hasJiraIssuesEnabled) => {
+ await setEnableCheckbox(hasJiraIssuesEnabled);
+
+ expect(findJiraForVulnerabilities().exists()).toBe(hasJiraIssuesEnabled);
+ },
+ );
+
+ it('passes down the correct initial-issue-type-id value when value is empty', async () => {
+ await setEnableCheckbox(true);
+ expect(findJiraForVulnerabilities().attributes('initial-issue-type-id')).toBeUndefined();
+ });
+
+ it('passes down the correct initial-issue-type-id value when value is not empty', async () => {
+ const jiraIssueType = 'some-jira-issue-type';
+ wrapper.setProps({ initialVulnerabilitiesIssuetype: jiraIssueType });
+ await setEnableCheckbox(true);
+ expect(findJiraForVulnerabilities().attributes('initial-issue-type-id')).toBe(
+ jiraIssueType,
+ );
+ });
+
+ it('emits "getJiraIssueTypes" to the eventHub when the jira-vulnerabilities component requests to fetch issue types', async () => {
+ const eventHubEmitSpy = jest.spyOn(eventHub, '$emit');
+
+ await setEnableCheckbox(true);
+ await findJiraForVulnerabilities().vm.$emit('request-get-issue-types');
+
+ expect(eventHubEmitSpy).toHaveBeenCalledWith('getJiraIssueTypes');
+ });
+
+ describe('with "jiraForVulnerabilities" feature flag disabled', () => {
+ beforeEach(async () => {
+ createComponent({
+ provide: { glFeatures: { jiraForVulnerabilities: false } },
+ });
+ });
+
+ it('does not show section', () => {
+ expect(findJiraForVulnerabilities().exists()).toBe(false);
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js b/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js
index a69e8d6e163..c6e7ee44355 100644
--- a/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js
+++ b/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js
@@ -1,5 +1,5 @@
-import { mount } from '@vue/test-utils';
import { GlFormCheckbox } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import JiraTriggerFields from '~/integrations/edit/components/jira_trigger_fields.vue';
describe('JiraTriggerFields', () => {
diff --git a/spec/frontend/integrations/edit/components/override_dropdown_spec.js b/spec/frontend/integrations/edit/components/override_dropdown_spec.js
index f312c456d5f..592f4514e45 100644
--- a/spec/frontend/integrations/edit/components/override_dropdown_spec.js
+++ b/spec/frontend/integrations/edit/components/override_dropdown_spec.js
@@ -1,9 +1,8 @@
-import { shallowMount } from '@vue/test-utils';
import { GlDropdown, GlLink } from '@gitlab/ui';
-import { createStore } from '~/integrations/edit/store';
-
-import { integrationLevels, overrideDropdownDescriptions } from '~/integrations/edit/constants';
+import { shallowMount } from '@vue/test-utils';
import OverrideDropdown from '~/integrations/edit/components/override_dropdown.vue';
+import { integrationLevels, overrideDropdownDescriptions } from '~/integrations/edit/constants';
+import { createStore } from '~/integrations/edit/store';
describe('OverrideDropdown', () => {
let wrapper;
diff --git a/spec/frontend/integrations/edit/components/trigger_fields_spec.js b/spec/frontend/integrations/edit/components/trigger_fields_spec.js
index 3fa1e5b5f5a..3e5326812b1 100644
--- a/spec/frontend/integrations/edit/components/trigger_fields_spec.js
+++ b/spec/frontend/integrations/edit/components/trigger_fields_spec.js
@@ -1,5 +1,5 @@
-import { mount } from '@vue/test-utils';
import { GlFormGroup, GlFormCheckbox, GlFormInput } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import TriggerFields from '~/integrations/edit/components/trigger_fields.vue';
describe('TriggerFields', () => {
diff --git a/spec/frontend/integrations/edit/store/actions_spec.js b/spec/frontend/integrations/edit/store/actions_spec.js
index 1ff881c265d..e2f4c138ece 100644
--- a/spec/frontend/integrations/edit/store/actions_spec.js
+++ b/spec/frontend/integrations/edit/store/actions_spec.js
@@ -1,6 +1,4 @@
import testAction from 'helpers/vuex_action_helper';
-import { refreshCurrentPage } from '~/lib/utils/url_utility';
-import createState from '~/integrations/edit/store/state';
import {
setOverride,
setIsSaving,
@@ -9,8 +7,13 @@ import {
requestResetIntegration,
receiveResetIntegrationSuccess,
receiveResetIntegrationError,
+ requestJiraIssueTypes,
+ receiveJiraIssueTypesSuccess,
+ receiveJiraIssueTypesError,
} from '~/integrations/edit/store/actions';
import * as types from '~/integrations/edit/store/mutation_types';
+import createState from '~/integrations/edit/store/state';
+import { refreshCurrentPage } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility');
@@ -70,4 +73,34 @@ describe('Integration form store actions', () => {
]);
});
});
+
+ describe('requestJiraIssueTypes', () => {
+ it('should commit SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE and SET_IS_LOADING_JIRA_ISSUE_TYPES mutations', () => {
+ return testAction(requestJiraIssueTypes, null, state, [
+ { type: types.SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE, payload: '' },
+ { type: types.SET_IS_LOADING_JIRA_ISSUE_TYPES, payload: true },
+ ]);
+ });
+ });
+
+ describe('receiveJiraIssueTypesSuccess', () => {
+ it('should commit SET_IS_LOADING_JIRA_ISSUE_TYPES and SET_JIRA_ISSUE_TYPES mutations', () => {
+ const issueTypes = ['issue', 'epic'];
+ return testAction(receiveJiraIssueTypesSuccess, issueTypes, state, [
+ { type: types.SET_IS_LOADING_JIRA_ISSUE_TYPES, payload: false },
+ { type: types.SET_JIRA_ISSUE_TYPES, payload: issueTypes },
+ ]);
+ });
+ });
+
+ describe('receiveJiraIssueTypesError', () => {
+ it('should commit SET_IS_LOADING_JIRA_ISSUE_TYPES, SET_JIRA_ISSUE_TYPES and SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE mutations', () => {
+ const errorMessage = 'something went wrong';
+ return testAction(receiveJiraIssueTypesError, errorMessage, state, [
+ { type: types.SET_IS_LOADING_JIRA_ISSUE_TYPES, payload: false },
+ { type: types.SET_JIRA_ISSUE_TYPES, payload: [] },
+ { type: types.SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE, payload: errorMessage },
+ ]);
+ });
+ });
});
diff --git a/spec/frontend/integrations/edit/store/getters_spec.js b/spec/frontend/integrations/edit/store/getters_spec.js
index 7d4532a1059..ad7a887dff2 100644
--- a/spec/frontend/integrations/edit/store/getters_spec.js
+++ b/spec/frontend/integrations/edit/store/getters_spec.js
@@ -4,9 +4,9 @@ import {
isDisabled,
propsSource,
} from '~/integrations/edit/store/getters';
-import createState from '~/integrations/edit/store/state';
-import mutations from '~/integrations/edit/store/mutations';
import * as types from '~/integrations/edit/store/mutation_types';
+import mutations from '~/integrations/edit/store/mutations';
+import createState from '~/integrations/edit/store/state';
import { mockIntegrationProps } from '../mock_data';
describe('Integration form store getters', () => {
diff --git a/spec/frontend/integrations/edit/store/mutations_spec.js b/spec/frontend/integrations/edit/store/mutations_spec.js
index 81f39adb87f..18faa2f6bba 100644
--- a/spec/frontend/integrations/edit/store/mutations_spec.js
+++ b/spec/frontend/integrations/edit/store/mutations_spec.js
@@ -1,6 +1,6 @@
+import * as types from '~/integrations/edit/store/mutation_types';
import mutations from '~/integrations/edit/store/mutations';
import createState from '~/integrations/edit/store/state';
-import * as types from '~/integrations/edit/store/mutation_types';
describe('Integration form store mutations', () => {
let state;
@@ -56,4 +56,30 @@ describe('Integration form store mutations', () => {
expect(state.isResetting).toBe(false);
});
});
+
+ describe(`${types.SET_JIRA_ISSUE_TYPES}`, () => {
+ it('sets jiraIssueTypes', () => {
+ const jiraIssueTypes = ['issue', 'epic'];
+ mutations[types.SET_JIRA_ISSUE_TYPES](state, jiraIssueTypes);
+
+ expect(state.jiraIssueTypes).toBe(jiraIssueTypes);
+ });
+ });
+
+ describe(`${types.SET_IS_LOADING_JIRA_ISSUE_TYPES}`, () => {
+ it.each([true, false])('sets isLoadingJiraIssueTypes to "%s"', (isLoading) => {
+ mutations[types.SET_IS_LOADING_JIRA_ISSUE_TYPES](state, isLoading);
+
+ expect(state.isLoadingJiraIssueTypes).toBe(isLoading);
+ });
+ });
+
+ describe(`${types.SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE}`, () => {
+ it('sets loadingJiraIssueTypesErrorMessage', () => {
+ const errorMessage = 'something went wrong';
+ mutations[types.SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE](state, errorMessage);
+
+ expect(state.loadingJiraIssueTypesErrorMessage).toBe(errorMessage);
+ });
+ });
});
diff --git a/spec/frontend/integrations/edit/store/state_spec.js b/spec/frontend/integrations/edit/store/state_spec.js
index 4d0f4a1da71..6cd84836395 100644
--- a/spec/frontend/integrations/edit/store/state_spec.js
+++ b/spec/frontend/integrations/edit/store/state_spec.js
@@ -9,6 +9,9 @@ describe('Integration form state factory', () => {
isTesting: false,
isResetting: false,
override: false,
+ isLoadingJiraIssueTypes: false,
+ jiraIssueTypes: [],
+ loadingJiraIssueTypesErrorMessage: '',
});
});
diff --git a/spec/frontend/integrations/integration_settings_form_spec.js b/spec/frontend/integrations/integration_settings_form_spec.js
index bba851ad796..348b942703f 100644
--- a/spec/frontend/integrations/integration_settings_form_spec.js
+++ b/spec/frontend/integrations/integration_settings_form_spec.js
@@ -1,6 +1,6 @@
import MockAdaptor from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
import IntegrationSettingsForm from '~/integrations/integration_settings_form';
+import axios from '~/lib/utils/axios_utils';
import toast from '~/vue_shared/plugins/global_toast';
jest.mock('~/vue_shared/plugins/global_toast');
@@ -132,4 +132,83 @@ describe('IntegrationSettingsForm', () => {
expect(dispatchSpy).toHaveBeenCalledWith('setIsTesting', false);
});
});
+
+ describe('getJiraIssueTypes', () => {
+ let integrationSettingsForm;
+ let formData;
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdaptor(axios);
+
+ jest.spyOn(axios, 'put');
+
+ integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
+ integrationSettingsForm.init();
+
+ // eslint-disable-next-line no-jquery/no-serialize
+ formData = integrationSettingsForm.$form.serialize();
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('should always dispatch `requestJiraIssueTypes`', async () => {
+ const dispatchSpy = jest.fn();
+
+ mock.onPut(integrationSettingsForm.testEndPoint).networkError();
+
+ integrationSettingsForm.vue.$store = { dispatch: dispatchSpy };
+
+ await integrationSettingsForm.getJiraIssueTypes();
+
+ expect(dispatchSpy).toHaveBeenCalledWith('requestJiraIssueTypes');
+ });
+
+ it('should make an ajax request with provided `formData`', async () => {
+ await integrationSettingsForm.getJiraIssueTypes(formData);
+
+ expect(axios.put).toHaveBeenCalledWith(integrationSettingsForm.testEndPoint, formData);
+ });
+
+ it('should dispatch `receiveJiraIssueTypesSuccess` with the correct payload if ajax request is successful', async () => {
+ const mockData = ['ISSUE', 'EPIC'];
+ const dispatchSpy = jest.fn();
+
+ mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
+ error: false,
+ issuetypes: mockData,
+ });
+
+ integrationSettingsForm.vue.$store = { dispatch: dispatchSpy };
+
+ await integrationSettingsForm.getJiraIssueTypes(formData);
+
+ expect(dispatchSpy).toHaveBeenCalledWith('receiveJiraIssueTypesSuccess', mockData);
+ });
+
+ it.each(['something went wrong', undefined])(
+ 'should dispatch "receiveJiraIssueTypesError" with a message if the backend responds with error',
+ async (responseErrorMessage) => {
+ const defaultErrorMessage = 'Connection failed. Please check your settings.';
+ const expectedErrorMessage = responseErrorMessage || defaultErrorMessage;
+ const dispatchSpy = jest.fn();
+
+ mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
+ error: true,
+ message: responseErrorMessage,
+ });
+
+ integrationSettingsForm.vue.$store = { dispatch: dispatchSpy };
+
+ await integrationSettingsForm.getJiraIssueTypes(formData);
+
+ expect(dispatchSpy).toHaveBeenCalledWith(
+ 'receiveJiraIssueTypesError',
+ expectedErrorMessage,
+ );
+ },
+ );
+ });
});
diff --git a/spec/frontend/invite_member/components/invite_member_modal_spec.js b/spec/frontend/invite_member/components/invite_member_modal_spec.js
index 1140c2a9475..4eff19402a8 100644
--- a/spec/frontend/invite_member/components/invite_member_modal_spec.js
+++ b/spec/frontend/invite_member/components/invite_member_modal_spec.js
@@ -1,7 +1,7 @@
-import { shallowMount } from '@vue/test-utils';
import { GlLink, GlModal } from '@gitlab/ui';
-import { mockTracking, unmockTracking, triggerEvent } from 'helpers/tracking_helper';
+import { shallowMount } from '@vue/test-utils';
import { stubComponent } from 'helpers/stub_component';
+import { mockTracking, unmockTracking, triggerEvent } from 'helpers/tracking_helper';
import InviteMemberModal from '~/invite_member/components/invite_member_modal.vue';
const memberPath = 'member_path';
diff --git a/spec/frontend/invite_member/components/invite_member_trigger_spec.js b/spec/frontend/invite_member/components/invite_member_trigger_spec.js
index 57b8918e3da..67c312fd155 100644
--- a/spec/frontend/invite_member/components/invite_member_trigger_spec.js
+++ b/spec/frontend/invite_member/components/invite_member_trigger_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { mockTracking, unmockTracking, triggerEvent } from 'helpers/tracking_helper';
import InviteMemberTrigger from '~/invite_member/components/invite_member_trigger.vue';
import triggerProvides from './invite_member_trigger_mock_data';
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 fc039bdf6da..e310a00133c 100644
--- a/spec/frontend/invite_members/components/invite_members_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlDropdown, GlDropdownItem, GlDatepicker, GlSprintf, GlLink, GlModal } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
diff --git a/spec/frontend/invite_members/components/invite_members_trigger_spec.js b/spec/frontend/invite_members/components/invite_members_trigger_spec.js
index 450d37a9748..18d6662d2d4 100644
--- a/spec/frontend/invite_members/components/invite_members_trigger_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_trigger_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlIcon, GlLink } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
const displayText = 'Invite team members';
diff --git a/spec/frontend/invite_members/components/members_token_select_spec.js b/spec/frontend/invite_members/components/members_token_select_spec.js
index ff123a13ce7..a945b99bd54 100644
--- a/spec/frontend/invite_members/components/members_token_select_spec.js
+++ b/spec/frontend/invite_members/components/members_token_select_spec.js
@@ -1,8 +1,8 @@
+import { GlTokenSelector } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import { GlTokenSelector } from '@gitlab/ui';
-import waitForPromises from 'helpers/wait_for_promises';
import { stubComponent } from 'helpers/stub_component';
+import waitForPromises from 'helpers/wait_for_promises';
import * as UserApi from '~/api/user_api';
import MembersTokenSelect from '~/invite_members/components/members_token_select.vue';
diff --git a/spec/frontend/issuable/components/issuable_by_email_spec.js b/spec/frontend/issuable/components/issuable_by_email_spec.js
new file mode 100644
index 00000000000..08a99f29479
--- /dev/null
+++ b/spec/frontend/issuable/components/issuable_by_email_spec.js
@@ -0,0 +1,164 @@
+import { GlModal, GlSprintf, GlFormInputGroup, GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
+import httpStatus from '~/lib/utils/http_status';
+
+const initialEmail = 'user@gitlab.com';
+
+const mockToastShow = jest.fn();
+
+describe('IssuableByEmail', () => {
+ let wrapper;
+ let mockAxios;
+ let glModalDirective;
+
+ function createComponent(injectedProperties = {}) {
+ glModalDirective = jest.fn();
+
+ return extendedWrapper(
+ shallowMount(IssuableByEmail, {
+ stubs: {
+ GlModal,
+ GlSprintf,
+ GlFormInputGroup,
+ GlButton,
+ },
+ directives: {
+ glModal: {
+ bind(_, { value }) {
+ glModalDirective(value);
+ },
+ },
+ },
+ mocks: {
+ $toast: {
+ show: mockToastShow,
+ },
+ },
+ provide: {
+ issuableType: 'issue',
+ initialEmail,
+ ...injectedProperties,
+ },
+ }),
+ );
+ }
+
+ beforeEach(() => {
+ mockAxios = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ mockAxios.restore();
+ });
+
+ const findFormInputGroup = () => wrapper.find(GlFormInputGroup);
+
+ const clickResetEmail = async () => {
+ wrapper.findByTestId('incoming-email-token-reset').vm.$emit('click');
+
+ await waitForPromises();
+ };
+
+ describe('modal button', () => {
+ it.each`
+ issuableType | buttonText
+ ${'issue'} | ${'Email a new issue to this project'}
+ ${'merge_request'} | ${'Email a new merge request to this project'}
+ `(
+ 'renders a link with "$buttonText" when type is "$issuableType"',
+ ({ issuableType, buttonText }) => {
+ wrapper = createComponent({ issuableType });
+ expect(wrapper.findByTestId('issuable-email-modal-btn').text()).toBe(buttonText);
+ },
+ );
+
+ it('opens the modal when the user clicks the button', () => {
+ wrapper = createComponent();
+
+ wrapper.findByTestId('issuable-email-modal-btn').vm.$emit('click');
+
+ expect(glModalDirective).toHaveBeenCalled();
+ });
+ });
+
+ describe('modal', () => {
+ it('renders a read-only email input field', () => {
+ wrapper = createComponent();
+
+ expect(findFormInputGroup().props('value')).toBe('user@gitlab.com');
+ });
+
+ it.each`
+ issuableType | subject | body
+ ${'issue'} | ${'Enter the issue title'} | ${'Enter the issue description'}
+ ${'merge_request'} | ${'Enter the merge request title'} | ${'Enter the merge request description'}
+ `('renders a mailto button when type is "$issuableType"', ({ issuableType, subject, body }) => {
+ wrapper = createComponent({
+ issuableType,
+ initialEmail,
+ });
+
+ expect(wrapper.findByTestId('mail-to-btn').attributes('href')).toBe(
+ `mailto:${initialEmail}?subject=${subject}&body=${body}`,
+ );
+ });
+
+ describe('reset email', () => {
+ const resetPath = 'gitlab-test/new_issuable_address?issuable_type=issue';
+
+ beforeEach(() => {
+ jest.spyOn(axios, 'put');
+ });
+ it('should send request to reset email token', async () => {
+ wrapper = createComponent({
+ issuableType: 'issue',
+ initialEmail,
+ resetPath,
+ });
+
+ await clickResetEmail();
+
+ expect(axios.put).toHaveBeenCalledWith(resetPath);
+ });
+
+ it('should update the email when the request succeeds', async () => {
+ mockAxios.onPut(resetPath).reply(httpStatus.OK, { new_address: 'foo@bar.com' });
+
+ wrapper = createComponent({
+ issuableType: 'issue',
+ initialEmail,
+ resetPath,
+ });
+
+ await clickResetEmail();
+
+ expect(findFormInputGroup().props('value')).toBe('foo@bar.com');
+ });
+
+ it('should show a toast message when the request fails', async () => {
+ mockAxios.onPut(resetPath).reply(httpStatus.NOT_FOUND, {});
+
+ wrapper = createComponent({
+ issuableType: 'issue',
+ initialEmail,
+ resetPath,
+ });
+
+ await clickResetEmail();
+
+ expect(mockToastShow).toHaveBeenCalledWith(
+ 'There was an error when reseting email token.',
+ { type: 'error' },
+ );
+ expect(findFormInputGroup().props('value')).toBe('user@gitlab.com');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js b/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js
index de2671816d8..173d12757e3 100644
--- a/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js
+++ b/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js
@@ -1,6 +1,6 @@
import { mount, shallowMount } from '@vue/test-utils';
-import { issuableTypesMap, linkedIssueTypesMap, PathIdSeparator } from '~/related_issues/constants';
import AddIssuableForm from '~/related_issues/components/add_issuable_form.vue';
+import { issuableTypesMap, linkedIssueTypesMap, PathIdSeparator } from '~/related_issues/constants';
const issuable1 = {
id: 200,
diff --git a/spec/frontend/issuable/related_issues/components/issue_token_spec.js b/spec/frontend/issuable/related_issues/components/issue_token_spec.js
index d5181d4a17a..d6aeacfe07a 100644
--- a/spec/frontend/issuable/related_issues/components/issue_token_spec.js
+++ b/spec/frontend/issuable/related_issues/components/issue_token_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import { PathIdSeparator } from '~/related_issues/constants';
import IssueToken from '~/related_issues/components/issue_token.vue';
+import { PathIdSeparator } from '~/related_issues/constants';
describe('IssueToken', () => {
const idKey = 200;
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 c0889b09adc..a450f912c4e 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
@@ -1,5 +1,5 @@
-import { shallowMount, mount } from '@vue/test-utils';
import { GlButton, GlIcon } from '@gitlab/ui';
+import { shallowMount, mount } from '@vue/test-utils';
import {
issuable1,
issuable2,
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 93fe321957c..e5e3478dc59 100644
--- a/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
+++ b/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
@@ -6,11 +6,11 @@ import {
issuable1,
issuable2,
} from 'jest/vue_shared/components/issue/related_issuable_mock_data';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
import RelatedIssuesRoot from '~/related_issues/components/related_issues_root.vue';
-import relatedIssuesService from '~/related_issues/services/related_issues_service';
import { linkedIssueTypesMap } from '~/related_issues/constants';
-import axios from '~/lib/utils/axios_utils';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import relatedIssuesService from '~/related_issues/services/related_issues_service';
jest.mock('~/flash');
diff --git a/spec/frontend/issuable_create/components/issuable_form_spec.js b/spec/frontend/issuable_create/components/issuable_form_spec.js
index e489d1dae3e..a074fddf091 100644
--- a/spec/frontend/issuable_create/components/issuable_form_spec.js
+++ b/spec/frontend/issuable_create/components/issuable_form_spec.js
@@ -1,11 +1,10 @@
-import { shallowMount } from '@vue/test-utils';
import { GlFormInput } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import IssuableForm from '~/issuable_create/components/issuable_form.vue';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
-import IssuableForm from '~/issuable_create/components/issuable_form.vue';
-
const createComponent = ({
descriptionPreviewPath = '/gitlab-org/gitlab-shell/preview_markdown',
descriptionHelpPath = '/help/user/markdown',
diff --git a/spec/frontend/issuable_list/components/issuable_item_spec.js b/spec/frontend/issuable_list/components/issuable_item_spec.js
index 3c01bf2d319..987acf559e3 100644
--- a/spec/frontend/issuable_list/components/issuable_item_spec.js
+++ b/spec/frontend/issuable_list/components/issuable_item_spec.js
@@ -1,6 +1,6 @@
-import { shallowMount } from '@vue/test-utils';
import { GlLink, GlLabel, GlIcon, GlFormCheckbox } from '@gitlab/ui';
-
+import { shallowMount } from '@vue/test-utils';
+import { useFakeDate } from 'helpers/fake_date';
import IssuableItem from '~/issuable_list/components/issuable_item.vue';
import IssuableAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
@@ -18,14 +18,19 @@ const createComponent = ({ issuableSymbol = '#', issuable = mockIssuable, slots
slots,
});
+const MOCK_GITLAB_URL = 'http://0.0.0.0:3000';
+
describe('IssuableItem', () => {
+ // The mock data is dependent that this is after our default date
+ useFakeDate(2020, 11, 11);
+
const mockLabels = mockIssuable.labels.nodes;
const mockAuthor = mockIssuable.author;
const originalUrl = gon.gitlab_url;
let wrapper;
beforeEach(() => {
- gon.gitlab_url = 'http://0.0.0.0:3000';
+ gon.gitlab_url = MOCK_GITLAB_URL;
wrapper = createComponent();
});
@@ -70,11 +75,11 @@ describe('IssuableItem', () => {
describe('isIssuableUrlExternal', () => {
it.each`
- issuableWebUrl | urlType | returnValue
- ${'/gitlab-org/gitlab-test/-/issues/2'} | ${'relative'} | ${false}
- ${'http://0.0.0.0:3000/gitlab-org/gitlab-test/-/issues/1'} | ${'absolute and internal'} | ${false}
- ${'http://jira.atlassian.net/browse/IG-1'} | ${'external'} | ${true}
- ${'https://github.com/gitlabhq/gitlabhq/issues/1'} | ${'external'} | ${true}
+ issuableWebUrl | urlType | returnValue
+ ${'/gitlab-org/gitlab-test/-/issues/2'} | ${'relative'} | ${false}
+ ${`${MOCK_GITLAB_URL}/gitlab-org/gitlab-test/-/issues/1`} | ${'absolute and internal'} | ${false}
+ ${'http://jira.atlassian.net/browse/IG-1'} | ${'external'} | ${true}
+ ${'https://github.com/gitlabhq/gitlabhq/issues/1'} | ${'external'} | ${true}
`(
'returns $returnValue when `issuable.webUrl` is $urlType',
async ({ issuableWebUrl, returnValue }) => {
@@ -214,14 +219,32 @@ describe('IssuableItem', () => {
});
describe('template', () => {
- it('renders issuable title', () => {
- const titleEl = wrapper.find('[data-testid="issuable-title"]');
+ it.each`
+ gitlabWebUrl | webUrl | expectedHref | expectedTarget
+ ${undefined} | ${`${MOCK_GITLAB_URL}/issue`} | ${`${MOCK_GITLAB_URL}/issue`} | ${undefined}
+ ${undefined} | ${'https://jira.com/issue'} | ${'https://jira.com/issue'} | ${'_blank'}
+ ${'/gitlab-org/issue'} | ${'https://jira.com/issue'} | ${'/gitlab-org/issue'} | ${undefined}
+ `(
+ 'renders issuable title correctly when `gitlabWebUrl` is `$gitlabWebUrl` and webUrl is `$webUrl`',
+ async ({ webUrl, gitlabWebUrl, expectedHref, expectedTarget }) => {
+ wrapper.setProps({
+ issuable: {
+ ...mockIssuable,
+ webUrl,
+ gitlabWebUrl,
+ },
+ });
- expect(titleEl.exists()).toBe(true);
- expect(titleEl.find(GlLink).attributes('href')).toBe(mockIssuable.webUrl);
- expect(titleEl.find(GlLink).attributes('target')).not.toBeDefined();
- expect(titleEl.find(GlLink).text()).toBe(mockIssuable.title);
- });
+ await wrapper.vm.$nextTick();
+
+ const titleEl = wrapper.find('[data-testid="issuable-title"]');
+
+ expect(titleEl.exists()).toBe(true);
+ expect(titleEl.find(GlLink).attributes('href')).toBe(expectedHref);
+ expect(titleEl.find(GlLink).attributes('target')).toBe(expectedTarget);
+ expect(titleEl.find(GlLink).text()).toBe(mockIssuable.title);
+ },
+ );
it('renders checkbox when `showCheckbox` prop is true', async () => {
wrapper.setProps({
@@ -257,6 +280,23 @@ describe('IssuableItem', () => {
);
});
+ it('renders issuable confidential icon when issuable is confidential', async () => {
+ wrapper.setProps({
+ issuable: {
+ ...mockIssuable,
+ confidential: true,
+ },
+ });
+
+ await wrapper.vm.$nextTick();
+
+ const confidentialEl = wrapper.find('[data-testid="issuable-title"]').find(GlIcon);
+
+ expect(confidentialEl.exists()).toBe(true);
+ expect(confidentialEl.props('name')).toBe('eye-slash');
+ expect(confidentialEl.attributes('title')).toBe('Confidential');
+ });
+
it('renders issuable reference', () => {
const referenceEl = wrapper.find('[data-testid="issuable-reference"]');
diff --git a/spec/frontend/issuable_list/components/issuable_list_root_spec.js b/spec/frontend/issuable_list/components/issuable_list_root_spec.js
index add5d9e8e2d..9c57233548c 100644
--- a/spec/frontend/issuable_list/components/issuable_list_root_spec.js
+++ b/spec/frontend/issuable_list/components/issuable_list_root_spec.js
@@ -1,11 +1,11 @@
-import { mount } from '@vue/test-utils';
import { GlSkeletonLoading, GlPagination } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
+import IssuableItem from '~/issuable_list/components/issuable_item.vue';
import IssuableListRoot from '~/issuable_list/components/issuable_list_root.vue';
import IssuableTabs from '~/issuable_list/components/issuable_tabs.vue';
-import IssuableItem from '~/issuable_list/components/issuable_item.vue';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import { mockIssuableListProps, mockIssuables } from '../mock_data';
diff --git a/spec/frontend/issuable_list/components/issuable_tabs_spec.js b/spec/frontend/issuable_list/components/issuable_tabs_spec.js
index 12611400084..3cc237b9ce9 100644
--- a/spec/frontend/issuable_list/components/issuable_tabs_spec.js
+++ b/spec/frontend/issuable_list/components/issuable_tabs_spec.js
@@ -1,5 +1,5 @@
-import { mount } from '@vue/test-utils';
import { GlTab, GlBadge } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import IssuableTabs from '~/issuable_list/components/issuable_tabs.vue';
diff --git a/spec/frontend/issuable_show/components/issuable_body_spec.js b/spec/frontend/issuable_show/components/issuable_body_spec.js
index 4ffbbad4f37..bf166bea1e5 100644
--- a/spec/frontend/issuable_show/components/issuable_body_spec.js
+++ b/spec/frontend/issuable_show/components/issuable_body_spec.js
@@ -1,10 +1,11 @@
import { shallowMount } from '@vue/test-utils';
+import { useFakeDate } from 'helpers/fake_date';
import IssuableBody from '~/issuable_show/components/issuable_body.vue';
-import IssuableTitle from '~/issuable_show/components/issuable_title.vue';
import IssuableDescription from '~/issuable_show/components/issuable_description.vue';
import IssuableEditForm from '~/issuable_show/components/issuable_edit_form.vue';
+import IssuableTitle from '~/issuable_show/components/issuable_title.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { mockIssuableShowProps, mockIssuable } from '../mock_data';
@@ -35,6 +36,9 @@ const createComponent = (propsData = issuableBodyProps) =>
});
describe('IssuableBody', () => {
+ // Some assertions expect a date later than our default
+ useFakeDate(2020, 11, 11);
+
let wrapper;
beforeEach(() => {
@@ -98,11 +102,8 @@ describe('IssuableBody', () => {
it('renders issuable edit info', () => {
const editedEl = wrapper.find('small');
- const sanitizedText = editedEl.text().replace(/\n/g, ' ').replace(/\s+/g, ' ');
- expect(sanitizedText).toContain('Edited');
- expect(sanitizedText).toContain('ago');
- expect(sanitizedText).toContain(`by ${mockIssuable.updatedBy.name}`);
+ expect(editedEl.text()).toMatchInterpolatedText('Edited 3 months ago by Administrator');
});
it('renders issuable-edit-form when `editFormVisible` prop is true', async () => {
diff --git a/spec/frontend/issuable_show/components/issuable_description_spec.js b/spec/frontend/issuable_show/components/issuable_description_spec.js
index 1dd8348b098..29ecce1002d 100644
--- a/spec/frontend/issuable_show/components/issuable_description_spec.js
+++ b/spec/frontend/issuable_show/components/issuable_description_spec.js
@@ -1,5 +1,5 @@
-import $ from 'jquery';
import { shallowMount } from '@vue/test-utils';
+import $ from 'jquery';
import IssuableDescription from '~/issuable_show/components/issuable_description.vue';
diff --git a/spec/frontend/issuable_show/components/issuable_edit_form_spec.js b/spec/frontend/issuable_show/components/issuable_edit_form_spec.js
index 522374f2e9c..184c9fe251c 100644
--- a/spec/frontend/issuable_show/components/issuable_edit_form_spec.js
+++ b/spec/frontend/issuable_show/components/issuable_edit_form_spec.js
@@ -1,9 +1,9 @@
-import { shallowMount } from '@vue/test-utils';
import { GlFormInput } from '@gitlab/ui';
-import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import { shallowMount } from '@vue/test-utils';
import IssuableEditForm from '~/issuable_show/components/issuable_edit_form.vue';
import IssuableEventHub from '~/issuable_show/event_hub';
+import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import { mockIssuableShowProps, mockIssuable } from '../mock_data';
diff --git a/spec/frontend/issuable_show/components/issuable_header_spec.js b/spec/frontend/issuable_show/components/issuable_header_spec.js
index f9c20ab04b8..2164caa40a8 100644
--- a/spec/frontend/issuable_show/components/issuable_header_spec.js
+++ b/spec/frontend/issuable_show/components/issuable_header_spec.js
@@ -1,5 +1,6 @@
-import { shallowMount } from '@vue/test-utils';
import { GlIcon, GlAvatarLabeled } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import IssuableHeader from '~/issuable_show/components/issuable_header.vue';
@@ -10,21 +11,23 @@ const issuableHeaderProps = {
...mockIssuableShowProps,
};
-const createComponent = (propsData = issuableHeaderProps) =>
- shallowMount(IssuableHeader, {
- propsData,
- slots: {
- 'status-badge': 'Open',
- 'header-actions': `
+const createComponent = (propsData = issuableHeaderProps, { stubs } = {}) =>
+ extendedWrapper(
+ shallowMount(IssuableHeader, {
+ propsData,
+ slots: {
+ 'status-badge': 'Open',
+ 'header-actions': `
<button class="js-close">Close issuable</button>
<a class="js-new" href="/gitlab-org/gitlab-shell/-/issues/new">New issuable</a>
`,
- },
- });
+ },
+ stubs,
+ }),
+ );
describe('IssuableHeader', () => {
let wrapper;
- const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`);
beforeEach(() => {
wrapper = createComponent();
@@ -63,7 +66,7 @@ describe('IssuableHeader', () => {
describe('template', () => {
it('renders issuable status icon and text', () => {
- const statusBoxEl = findByTestId('status');
+ const statusBoxEl = wrapper.findByTestId('status');
expect(statusBoxEl.exists()).toBe(true);
expect(statusBoxEl.find(GlIcon).props('name')).toBe(mockIssuableShowProps.statusIcon);
@@ -77,7 +80,7 @@ describe('IssuableHeader', () => {
await wrapper.vm.$nextTick();
- const blockedEl = findByTestId('blocked');
+ const blockedEl = wrapper.findByTestId('blocked');
expect(blockedEl.exists()).toBe(true);
expect(blockedEl.find(GlIcon).props('name')).toBe('lock');
@@ -90,7 +93,7 @@ describe('IssuableHeader', () => {
await wrapper.vm.$nextTick();
- const confidentialEl = findByTestId('confidential');
+ const confidentialEl = wrapper.findByTestId('confidential');
expect(confidentialEl.exists()).toBe(true);
expect(confidentialEl.find(GlIcon).props('name')).toBe('eye-slash');
@@ -105,7 +108,7 @@ describe('IssuableHeader', () => {
href: webUrl,
target: '_blank',
};
- const avatarEl = findByTestId('avatar');
+ const avatarEl = wrapper.findByTestId('avatar');
expect(avatarEl.exists()).toBe(true);
expect(avatarEl.attributes()).toMatchObject(avatarElAttrs);
expect(avatarEl.find(GlAvatarLabeled).attributes()).toMatchObject({
@@ -113,20 +116,46 @@ describe('IssuableHeader', () => {
src: avatarUrl,
label: name,
});
+ expect(avatarEl.find(GlAvatarLabeled).find(GlIcon).exists()).toBe(false);
});
it('renders sidebar toggle button', () => {
- const toggleButtonEl = findByTestId('sidebar-toggle');
+ const toggleButtonEl = wrapper.findByTestId('sidebar-toggle');
expect(toggleButtonEl.exists()).toBe(true);
expect(toggleButtonEl.props('icon')).toBe('chevron-double-lg-left');
});
it('renders header actions', () => {
- const actionsEl = findByTestId('header-actions');
+ const actionsEl = wrapper.findByTestId('header-actions');
expect(actionsEl.find('button.js-close').exists()).toBe(true);
expect(actionsEl.find('a.js-new').exists()).toBe(true);
});
+
+ describe('when author exists outside of GitLab', () => {
+ it("renders 'external-link' icon in avatar label", () => {
+ wrapper = createComponent(
+ {
+ ...issuableHeaderProps,
+ author: {
+ ...issuableHeaderProps.author,
+ webUrl: 'https://jira.com/test-user/author.jpg',
+ },
+ },
+ {
+ stubs: {
+ GlAvatarLabeled,
+ },
+ },
+ );
+
+ const avatarEl = wrapper.findComponent(GlAvatarLabeled);
+ const icon = avatarEl.find(GlIcon);
+
+ expect(icon.exists()).toBe(true);
+ expect(icon.props('name')).toBe('external-link');
+ });
+ });
});
});
diff --git a/spec/frontend/issuable_show/components/issuable_show_root_spec.js b/spec/frontend/issuable_show/components/issuable_show_root_spec.js
index 6cebfc150f9..3e3778492d2 100644
--- a/spec/frontend/issuable_show/components/issuable_show_root_spec.js
+++ b/spec/frontend/issuable_show/components/issuable_show_root_spec.js
@@ -1,9 +1,9 @@
import { shallowMount } from '@vue/test-utils';
+import IssuableBody from '~/issuable_show/components/issuable_body.vue';
+import IssuableHeader from '~/issuable_show/components/issuable_header.vue';
import IssuableShowRoot from '~/issuable_show/components/issuable_show_root.vue';
-import IssuableHeader from '~/issuable_show/components/issuable_header.vue';
-import IssuableBody from '~/issuable_show/components/issuable_body.vue';
import IssuableSidebar from '~/issuable_sidebar/components/issuable_sidebar_root.vue';
import { mockIssuableShowProps, mockIssuable } from '../mock_data';
diff --git a/spec/frontend/issuable_show/components/issuable_title_spec.js b/spec/frontend/issuable_show/components/issuable_title_spec.js
index e8621c763b3..df6fbdea76b 100644
--- a/spec/frontend/issuable_show/components/issuable_title_spec.js
+++ b/spec/frontend/issuable_show/components/issuable_title_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlIcon, GlButton, GlIntersectionObserver } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import IssuableTitle from '~/issuable_show/components/issuable_title.vue';
diff --git a/spec/frontend/issuable_sidebar/components/issuable_sidebar_root_spec.js b/spec/frontend/issuable_sidebar/components/issuable_sidebar_root_spec.js
index 7686dad4644..62a0016d67b 100644
--- a/spec/frontend/issuable_sidebar/components/issuable_sidebar_root_spec.js
+++ b/spec/frontend/issuable_sidebar/components/issuable_sidebar_root_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
+import { shallowMount } from '@vue/test-utils';
import Cookies from 'js-cookie';
import IssuableSidebarRoot from '~/issuable_sidebar/components/issuable_sidebar_root.vue';
diff --git a/spec/frontend/issuable_spec.js b/spec/frontend/issuable_spec.js
index 6712b8bfd34..9c8f1e04609 100644
--- a/spec/frontend/issuable_spec.js
+++ b/spec/frontend/issuable_spec.js
@@ -1,6 +1,3 @@
-import $ from 'jquery';
-import MockAdaptor from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
import IssuableIndex from '~/issuable_index';
import issuableInitBulkUpdateSidebar from '~/issuable_init_bulk_update_sidebar';
@@ -22,43 +19,4 @@ describe('Issuable', () => {
expect(issuableInitBulkUpdateSidebar.bulkUpdateSidebar).toBeDefined();
});
});
-
- describe('resetIncomingEmailToken', () => {
- let mock;
-
- beforeEach(() => {
- const element = document.createElement('a');
- element.classList.add('incoming-email-token-reset');
- element.setAttribute('href', 'foo');
- document.body.appendChild(element);
-
- const input = document.createElement('input');
- input.setAttribute('id', 'issuable_email');
- document.body.appendChild(input);
-
- new IssuableIndex('issue_'); // eslint-disable-line no-new
-
- mock = new MockAdaptor(axios);
-
- mock.onPut('foo').reply(200, {
- new_address: 'testing123',
- });
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- it('should send request to reset email token', (done) => {
- jest.spyOn(axios, 'put');
- document.querySelector('.incoming-email-token-reset').click();
-
- setImmediate(() => {
- expect(axios.put).toHaveBeenCalledWith('foo');
- expect($('#issuable_email').val()).toBe('testing123');
-
- done();
- });
- });
- });
});
diff --git a/spec/frontend/issuable_suggestions/components/item_spec.js b/spec/frontend/issuable_suggestions/components/item_spec.js
index d0dde256edd..39083b3d8fb 100644
--- a/spec/frontend/issuable_suggestions/components/item_spec.js
+++ b/spec/frontend/issuable_suggestions/components/item_spec.js
@@ -1,8 +1,8 @@
-import { shallowMount } from '@vue/test-utils';
import { GlTooltip, GlLink, GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
-import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
import Suggestion from '~/issuable_suggestions/components/item.vue';
+import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
import mockData from '../mock_data';
describe('Issuable suggestions suggestion component', () => {
diff --git a/spec/frontend/issue_show/components/app_spec.js b/spec/frontend/issue_show/components/app_spec.js
index ec2055ca7d1..9e1bc8242fe 100644
--- a/spec/frontend/issue_show/components/app_spec.js
+++ b/spec/frontend/issue_show/components/app_spec.js
@@ -2,11 +2,15 @@ import { GlIntersectionObserver } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { useMockIntersectionObserver } from 'helpers/mock_dom_observer';
-import axios from '~/lib/utils/axios_utils';
-import { visitUrl } from '~/lib/utils/url_utility';
import '~/behaviors/markdown/render_gfm';
import IssuableApp from '~/issue_show/components/app.vue';
+import DescriptionComponent from '~/issue_show/components/description.vue';
+import IncidentTabs from '~/issue_show/components/incidents/incident_tabs.vue';
+import PinnedLinks from '~/issue_show/components/pinned_links.vue';
+import { IssuableStatus, IssuableStatusText } from '~/issue_show/constants';
import eventHub from '~/issue_show/event_hub';
+import axios from '~/lib/utils/axios_utils';
+import { visitUrl } from '~/lib/utils/url_utility';
import {
appProps,
initialRequest,
@@ -14,10 +18,6 @@ import {
secondRequest,
zoomMeetingUrl,
} from '../mock_data';
-import IncidentTabs from '~/issue_show/components/incidents/incident_tabs.vue';
-import DescriptionComponent from '~/issue_show/components/description.vue';
-import PinnedLinks from '~/issue_show/components/pinned_links.vue';
-import { IssuableStatus, IssuableStatusText } from '~/issue_show/constants';
function formatText(text) {
return text.trim().replace(/\s\s+/g, ' ');
@@ -423,7 +423,7 @@ describe('Issuable output', () => {
});
it('shows the form if template names request is successful', () => {
- const mockData = [{ name: 'Bug' }];
+ const mockData = [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }];
mock.onGet('/issuable-templates-path').reply(() => Promise.resolve([200, mockData]));
return wrapper.vm.requestTemplatesAndShowForm().then(() => {
diff --git a/spec/frontend/issue_show/components/description_spec.js b/spec/frontend/issue_show/components/description_spec.js
index de53d98e6be..d59a257a2be 100644
--- a/spec/frontend/issue_show/components/description_spec.js
+++ b/spec/frontend/issue_show/components/description_spec.js
@@ -1,8 +1,8 @@
import $ from 'jquery';
import Vue from 'vue';
import '~/behaviors/markdown/render_gfm';
-import mountComponent from 'helpers/vue_mount_component_helper';
import { TEST_HOST } from 'helpers/test_constants';
+import mountComponent from 'helpers/vue_mount_component_helper';
import Description from '~/issue_show/components/description.vue';
import TaskList from '~/task_list';
import { descriptionProps as props } from '../mock_data';
diff --git a/spec/frontend/issue_show/components/fields/description_spec.js b/spec/frontend/issue_show/components/fields/description_spec.js
index 96c81c419d0..a50be30cf4c 100644
--- a/spec/frontend/issue_show/components/fields/description_spec.js
+++ b/spec/frontend/issue_show/components/fields/description_spec.js
@@ -1,70 +1,70 @@
-import Vue from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import DescriptionField from '~/issue_show/components/fields/description.vue';
import eventHub from '~/issue_show/event_hub';
-import Store from '~/issue_show/stores';
-import descriptionField from '~/issue_show/components/fields/description.vue';
-import { keyboardDownEvent } from '../../helpers';
+import MarkdownField from '~/vue_shared/components/markdown/field.vue';
describe('Description field component', () => {
- let vm;
- let store;
-
- beforeEach((done) => {
- const Component = Vue.extend(descriptionField);
- const el = document.createElement('div');
- store = new Store({
- titleHtml: '',
- descriptionHtml: '',
- issuableRef: '',
- });
- store.formState.description = 'test';
-
- document.body.appendChild(el);
+ let wrapper;
- jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ const findTextarea = () => wrapper.find({ ref: 'textarea' });
- vm = new Component({
- el,
+ const mountComponent = (description = 'test') =>
+ shallowMount(DescriptionField, {
+ attachTo: document.body,
propsData: {
markdownPreviewPath: '/',
markdownDocsPath: '/',
- formState: store.formState,
+ formState: {
+ description,
+ },
+ },
+ stubs: {
+ MarkdownField,
},
- }).$mount();
+ });
+
+ beforeEach(() => {
+ jest.spyOn(eventHub, '$emit');
+ });
- Vue.nextTick(done);
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
});
it('renders markdown field with description', () => {
- expect(vm.$el.querySelector('.md-area textarea').value).toBe('test');
+ wrapper = mountComponent();
+
+ expect(findTextarea().element.value).toBe('test');
});
- it('renders markdown field with a markdown description', (done) => {
- store.formState.description = '**test**';
+ it('renders markdown field with a markdown description', () => {
+ const markdown = '**test**';
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.md-area textarea').value).toBe('**test**');
+ wrapper = mountComponent(markdown);
- done();
- });
+ expect(findTextarea().element.value).toBe(markdown);
});
it('focuses field when mounted', () => {
- expect(document.activeElement).toBe(vm.$refs.textarea);
+ wrapper = mountComponent();
+
+ expect(document.activeElement).toBe(findTextarea().element);
});
it('triggers update with meta+enter', () => {
- vm.$el.querySelector('.md-area textarea').dispatchEvent(keyboardDownEvent(13, true));
+ wrapper = mountComponent();
- expect(eventHub.$emit).toHaveBeenCalled();
+ findTextarea().trigger('keydown.enter', { metaKey: true });
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('update.issuable');
});
it('triggers update with ctrl+enter', () => {
- vm.$el.querySelector('.md-area textarea').dispatchEvent(keyboardDownEvent(13, false, true));
+ wrapper = mountComponent();
- expect(eventHub.$emit).toHaveBeenCalled();
- });
+ findTextarea().trigger('keydown.enter', { ctrlKey: true });
- it('has a ref named `textarea`', () => {
- expect(vm.$refs.textarea).not.toBeNull();
+ expect(eventHub.$emit).toHaveBeenCalledWith('update.issuable');
});
});
diff --git a/spec/frontend/issue_show/components/fields/description_template_spec.js b/spec/frontend/issue_show/components/fields/description_template_spec.js
index 9ebab31f1ad..1193d4f8add 100644
--- a/spec/frontend/issue_show/components/fields/description_template_spec.js
+++ b/spec/frontend/issue_show/components/fields/description_template_spec.js
@@ -14,8 +14,10 @@ describe('Issue description template component', () => {
vm = new Component({
propsData: {
formState,
- issuableTemplates: [{ name: 'test' }],
+ issuableTemplates: [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }],
+ projectId: 1,
projectPath: '/',
+ namespacePath: '/',
projectNamespace: '/',
},
}).$mount();
@@ -23,7 +25,7 @@ describe('Issue description template component', () => {
it('renders templates as JSON array in data attribute', () => {
expect(vm.$el.querySelector('.js-issuable-selector').getAttribute('data-data')).toBe(
- '[{"name":"test"}]',
+ '[{"name":"test","id":"test","project_path":"/","namespace_path":"/"}]',
);
});
diff --git a/spec/frontend/issue_show/components/fields/title_spec.js b/spec/frontend/issue_show/components/fields/title_spec.js
index 99e8658b89f..783ce9eb76c 100644
--- a/spec/frontend/issue_show/components/fields/title_spec.js
+++ b/spec/frontend/issue_show/components/fields/title_spec.js
@@ -1,48 +1,42 @@
-import Vue from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import TitleField from '~/issue_show/components/fields/title.vue';
import eventHub from '~/issue_show/event_hub';
-import Store from '~/issue_show/stores';
-import titleField from '~/issue_show/components/fields/title.vue';
-import { keyboardDownEvent } from '../../helpers';
describe('Title field component', () => {
- let vm;
- let store;
+ let wrapper;
- beforeEach(() => {
- const Component = Vue.extend(titleField);
- store = new Store({
- titleHtml: '',
- descriptionHtml: '',
- issuableRef: '',
- });
- store.formState.title = 'test';
+ const findInput = () => wrapper.find({ ref: 'input' });
- jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ beforeEach(() => {
+ jest.spyOn(eventHub, '$emit');
- vm = new Component({
+ wrapper = shallowMount(TitleField, {
propsData: {
- formState: store.formState,
+ formState: {
+ title: 'test',
+ },
},
- }).$mount();
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
});
it('renders form control with formState title', () => {
- expect(vm.$el.querySelector('.form-control').value).toBe('test');
+ expect(findInput().element.value).toBe('test');
});
it('triggers update with meta+enter', () => {
- vm.$el.querySelector('.form-control').dispatchEvent(keyboardDownEvent(13, true));
+ findInput().trigger('keydown.enter', { metaKey: true });
- expect(eventHub.$emit).toHaveBeenCalled();
+ expect(eventHub.$emit).toHaveBeenCalledWith('update.issuable');
});
it('triggers update with ctrl+enter', () => {
- vm.$el.querySelector('.form-control').dispatchEvent(keyboardDownEvent(13, false, true));
-
- expect(eventHub.$emit).toHaveBeenCalled();
- });
+ findInput().trigger('keydown.enter', { ctrlKey: true });
- it('has a ref named `input`', () => {
- expect(vm.$refs.input).not.toBeNull();
+ expect(eventHub.$emit).toHaveBeenCalledWith('update.issuable');
});
});
diff --git a/spec/frontend/issue_show/components/form_spec.js b/spec/frontend/issue_show/components/form_spec.js
index 4e123f606f6..4a8ec3cf66a 100644
--- a/spec/frontend/issue_show/components/form_spec.js
+++ b/spec/frontend/issue_show/components/form_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
-import formComponent from '~/issue_show/components/form.vue';
import Autosave from '~/autosave';
+import formComponent from '~/issue_show/components/form.vue';
import eventHub from '~/issue_show/event_hub';
jest.mock('~/autosave');
@@ -19,6 +19,7 @@ describe('Inline edit form component', () => {
markdownPreviewPath: '/',
markdownDocsPath: '/',
projectPath: '/',
+ projectId: 1,
projectNamespace: '/',
};
@@ -42,7 +43,11 @@ describe('Inline edit form component', () => {
});
it('renders template selector when templates exists', () => {
- createComponent({ issuableTemplates: ['test'] });
+ createComponent({
+ issuableTemplates: [
+ { name: 'test', id: 'test', project_path: 'test', namespace_path: 'test' },
+ ],
+ });
expect(vm.$el.querySelector('.js-issuable-selector-wrap')).not.toBeNull();
});
diff --git a/spec/frontend/issue_show/components/incidents/highlight_bar_spec.js b/spec/frontend/issue_show/components/incidents/highlight_bar_spec.js
index 112cb4d4c3a..6758e6192b8 100644
--- a/spec/frontend/issue_show/components/incidents/highlight_bar_spec.js
+++ b/spec/frontend/issue_show/components/incidents/highlight_bar_spec.js
@@ -1,6 +1,6 @@
+import { GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import merge from 'lodash/merge';
-import { GlLink } from '@gitlab/ui';
import HighlightBar from '~/issue_show/components/incidents/highlight_bar.vue';
import { formatDate } from '~/lib/utils/datetime_utility';
diff --git a/spec/frontend/issue_show/components/incidents/incident_tabs_spec.js b/spec/frontend/issue_show/components/incidents/incident_tabs_spec.js
index 416870d1408..f46b6ba6f54 100644
--- a/spec/frontend/issue_show/components/incidents/incident_tabs_spec.js
+++ b/spec/frontend/issue_show/components/incidents/incident_tabs_spec.js
@@ -1,15 +1,15 @@
+import { GlTab } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import merge from 'lodash/merge';
-import { GlTab } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
-import INVALID_URL from '~/lib/utils/invalid_url';
-import IncidentTabs from '~/issue_show/components/incidents/incident_tabs.vue';
-import { descriptionProps } from '../../mock_data';
+import { trackIncidentDetailsViewsOptions } from '~/incidents/constants';
import DescriptionComponent from '~/issue_show/components/description.vue';
import HighlightBar from '~/issue_show/components/incidents/highlight_bar.vue';
-import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
+import IncidentTabs from '~/issue_show/components/incidents/incident_tabs.vue';
+import INVALID_URL from '~/lib/utils/invalid_url';
import Tracking from '~/tracking';
-import { trackIncidentDetailsViewsOptions } from '~/incidents/constants';
+import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
+import { descriptionProps } from '../../mock_data';
const mockAlert = {
__typename: 'AlertManagementAlert',
diff --git a/spec/frontend/issue_show/components/pinned_links_spec.js b/spec/frontend/issue_show/components/pinned_links_spec.js
index 2d140fd068a..3fe1f9fd6d9 100644
--- a/spec/frontend/issue_show/components/pinned_links_spec.js
+++ b/spec/frontend/issue_show/components/pinned_links_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import PinnedLinks from '~/issue_show/components/pinned_links.vue';
import { STATUS_PAGE_PUBLISHED, JOIN_ZOOM_MEETING } from '~/issue_show/constants';
diff --git a/spec/frontend/issue_show/components/title_spec.js b/spec/frontend/issue_show/components/title_spec.js
index c274048fdd5..78880a7f540 100644
--- a/spec/frontend/issue_show/components/title_spec.js
+++ b/spec/frontend/issue_show/components/title_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
-import Store from '~/issue_show/stores';
import titleComponent from '~/issue_show/components/title.vue';
import eventHub from '~/issue_show/event_hub';
+import Store from '~/issue_show/stores';
describe('Title component', () => {
let vm;
diff --git a/spec/frontend/issue_show/helpers.js b/spec/frontend/issue_show/helpers.js
deleted file mode 100644
index 7ca6a22929d..00000000000
--- a/spec/frontend/issue_show/helpers.js
+++ /dev/null
@@ -1,9 +0,0 @@
-export const keyboardDownEvent = (code, metaKey = false, ctrlKey = false) => {
- const e = new CustomEvent('keydown');
-
- e.keyCode = code;
- e.metaKey = metaKey;
- e.ctrlKey = ctrlKey;
-
- return e;
-};
diff --git a/spec/frontend/issue_show/issue_spec.js b/spec/frontend/issue_show/issue_spec.js
index 818f501882b..9cb7059dd7f 100644
--- a/spec/frontend/issue_show/issue_spec.js
+++ b/spec/frontend/issue_show/issue_spec.js
@@ -1,11 +1,11 @@
import MockAdapter from 'axios-mock-adapter';
import { useMockIntersectionObserver } from 'helpers/mock_dom_observer';
import waitForPromises from 'helpers/wait_for_promises';
-import axios from '~/lib/utils/axios_utils';
import { initIssuableApp } from '~/issue_show/issue';
import * as parseData from '~/issue_show/utils/parse_data';
-import { appProps } from './mock_data';
+import axios from '~/lib/utils/axios_utils';
import createStore from '~/notes/stores';
+import { appProps } from './mock_data';
const mock = new MockAdapter(axios);
mock.onGet().reply(200);
diff --git a/spec/frontend/issue_show/mock_data.js b/spec/frontend/issue_show/mock_data.js
index 5a31a550088..fd08c95b454 100644
--- a/spec/frontend/issue_show/mock_data.js
+++ b/spec/frontend/issue_show/mock_data.js
@@ -52,6 +52,7 @@ export const appProps = {
markdownDocsPath: '/',
projectNamespace: '/',
projectPath: '/',
+ projectId: 1,
issuableTemplateNamesPath: '/issuable-templates-path',
zoomMeetingUrl,
publishedIncidentUrl,
diff --git a/spec/frontend/issue_spec.js b/spec/frontend/issue_spec.js
index 303267e784f..fb6caef41e2 100644
--- a/spec/frontend/issue_spec.js
+++ b/spec/frontend/issue_spec.js
@@ -1,7 +1,7 @@
-import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
+import $ from 'jquery';
import Issue from '~/issue';
+import axios from '~/lib/utils/axios_utils';
import '~/lib/utils/text_utility';
describe('Issue', () => {
diff --git a/spec/frontend/issues_list/components/issuable_spec.js b/spec/frontend/issues_list/components/issuable_spec.js
index b47a84ad7f6..a8bf124373b 100644
--- a/spec/frontend/issues_list/components/issuable_spec.js
+++ b/spec/frontend/issues_list/components/issuable_spec.js
@@ -1,20 +1,26 @@
-import { shallowMount } from '@vue/test-utils';
import { GlSprintf, GlLabel, GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
import { trimText } from 'helpers/text_helper';
-import initUserPopovers from '~/user_popovers';
+import Issuable from '~/issues_list/components/issuable.vue';
+import { isScopedLabel } from '~/lib/utils/common_utils';
import { formatDate } from '~/lib/utils/datetime_utility';
import { mergeUrlParams } from '~/lib/utils/url_utility';
-import Issuable from '~/issues_list/components/issuable.vue';
+import initUserPopovers from '~/user_popovers';
import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
import { simpleIssue, testAssignees, testLabels } from '../issuable_list_test_data';
-import { isScopedLabel } from '~/lib/utils/common_utils';
jest.mock('~/user_popovers');
-const TEST_NOW = '2019-08-28T20:03:04.713Z';
-const TEST_MONTH_AGO = '2019-07-28';
-const TEST_MONTH_LATER = '2019-09-30';
+const TODAY = new Date();
+
+const createTestDateFromDelta = (timeDelta) =>
+ formatDate(new Date(TODAY.getTime() + timeDelta), 'yyyy-mm-dd');
+
+// TODO: Encapsulate date helpers https://gitlab.com/gitlab-org/gitlab/-/issues/320883
+const MONTHS_IN_MS = 1000 * 60 * 60 * 24 * 31;
+const TEST_MONTH_AGO = createTestDateFromDelta(-MONTHS_IN_MS);
+const TEST_MONTH_LATER = createTestDateFromDelta(MONTHS_IN_MS);
const DATE_FORMAT = 'mmm d, yyyy';
const TEST_USER_NAME = 'Tyler Durden';
const TEST_BASE_URL = `${TEST_HOST}/issues`;
@@ -26,16 +32,8 @@ const TEST_MILESTONE = {
const TEXT_CLOSED = 'CLOSED';
const TEST_META_COUNT = 100;
-// Use FixedDate so that time sensitive info in snapshots don't fail
-class FixedDate extends Date {
- constructor(date = TEST_NOW) {
- super(date);
- }
-}
-
describe('Issuable component', () => {
let issuable;
- let DateOrig;
let wrapper;
const factory = (props = {}, scopedLabelsAvailable = false) => {
@@ -63,15 +61,6 @@ describe('Issuable component', () => {
wrapper = null;
});
- beforeAll(() => {
- DateOrig = window.Date;
- window.Date = FixedDate;
- });
-
- afterAll(() => {
- window.Date = DateOrig;
- });
-
const checkExists = (findFn) => () => findFn().exists();
const hasIcon = (iconName, iconWrapper = wrapper) =>
iconWrapper.findAll(GlIcon).wrappers.some((icon) => icon.props('name') === iconName);
diff --git a/spec/frontend/issues_list/components/issuables_list_app_spec.js b/spec/frontend/issues_list/components/issuables_list_app_spec.js
index 9d603099ff7..fe3d2114463 100644
--- a/spec/frontend/issues_list/components/issuables_list_app_spec.js
+++ b/spec/frontend/issues_list/components/issuables_list_app_spec.js
@@ -1,19 +1,19 @@
-import axios from 'axios';
-import MockAdapter from 'axios-mock-adapter';
-import { shallowMount } from '@vue/test-utils';
import {
GlEmptyState,
GlPagination,
GlDeprecatedSkeletonLoading as GlSkeletonLoading,
} from '@gitlab/ui';
-import waitForPromises from 'helpers/wait_for_promises';
+import { shallowMount } from '@vue/test-utils';
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
+import waitForPromises from 'helpers/wait_for_promises';
import { deprecatedCreateFlash as flash } from '~/flash';
-import IssuablesListApp from '~/issues_list/components/issuables_list_app.vue';
import Issuable from '~/issues_list/components/issuable.vue';
-import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
-import issueablesEventBus from '~/issues_list/eventhub';
+import IssuablesListApp from '~/issues_list/components/issuables_list_app.vue';
import { PAGE_SIZE, PAGE_SIZE_MANUAL, RELATIVE_POSITION } from '~/issues_list/constants';
+import issueablesEventBus from '~/issues_list/eventhub';
+import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
jest.mock('~/flash');
jest.mock('~/issues_list/eventhub');
@@ -591,5 +591,75 @@ describe('Issuables list component', () => {
expect(findFilteredSearchBar().props('initialFilterValue')).toEqual(['free text']);
});
});
+
+ describe('on filter search', () => {
+ beforeEach(() => {
+ factory({ type: 'jira' });
+
+ window.history.pushState = jest.fn();
+ });
+
+ afterEach(() => {
+ window.history.pushState.mockRestore();
+ });
+
+ const emitOnFilter = (filter) => findFilteredSearchBar().vm.$emit('onFilter', filter);
+
+ describe('empty filter', () => {
+ const mockFilter = [];
+
+ it('updates URL with correct params', () => {
+ emitOnFilter(mockFilter);
+
+ expect(window.history.pushState).toHaveBeenCalledWith(
+ {},
+ '',
+ `${TEST_LOCATION}?state=opened`,
+ );
+ });
+ });
+
+ describe('filter with search term', () => {
+ const mockFilter = [
+ {
+ type: 'filtered-search-term',
+ value: { data: 'free' },
+ },
+ ];
+
+ it('updates URL with correct params', () => {
+ emitOnFilter(mockFilter);
+
+ expect(window.history.pushState).toHaveBeenCalledWith(
+ {},
+ '',
+ `${TEST_LOCATION}?state=opened&search=free`,
+ );
+ });
+ });
+
+ describe('filter with multiple search terms', () => {
+ const mockFilter = [
+ {
+ type: 'filtered-search-term',
+ value: { data: 'free' },
+ },
+ {
+ type: 'filtered-search-term',
+ value: { data: 'text' },
+ },
+ ];
+
+ it('updates URL with correct params', () => {
+ emitOnFilter(mockFilter);
+
+ expect(window.history.pushState).toHaveBeenCalledWith(
+ {},
+ '',
+ `${TEST_LOCATION}?state=opened&search=free+text`,
+ );
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/jira_connect/api_spec.js b/spec/frontend/jira_connect/api_spec.js
index 8fecbee9ca7..240a57c7917 100644
--- a/spec/frontend/jira_connect/api_spec.js
+++ b/spec/frontend/jira_connect/api_spec.js
@@ -1,9 +1,8 @@
import MockAdapter from 'axios-mock-adapter';
+import { addSubscription, removeSubscription, fetchGroups } from '~/jira_connect/api';
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
-import { addSubscription, removeSubscription, fetchGroups } from '~/jira_connect/api';
-
describe('JiraConnect API', () => {
let mock;
let response;
@@ -14,7 +13,7 @@ describe('JiraConnect API', () => {
const mockJwt = 'jwt';
const mockResponse = { success: true };
- const tokenSpy = jest.fn().mockReturnValue(mockJwt);
+ const tokenSpy = jest.fn((callback) => callback(mockJwt));
window.AP = {
context: {
diff --git a/spec/frontend/jira_connect/components/app_spec.js b/spec/frontend/jira_connect/components/app_spec.js
index be990d5061c..d11b66b2089 100644
--- a/spec/frontend/jira_connect/components/app_spec.js
+++ b/spec/frontend/jira_connect/components/app_spec.js
@@ -1,19 +1,20 @@
-import Vue from 'vue';
-import Vuex from 'vuex';
+import { GlAlert, GlButton, GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import { GlAlert } from '@gitlab/ui';
+
import JiraConnectApp from '~/jira_connect/components/app.vue';
import createStore from '~/jira_connect/store';
import { SET_ERROR_MESSAGE } from '~/jira_connect/store/mutation_types';
-Vue.use(Vuex);
+jest.mock('~/jira_connect/api');
describe('JiraConnectApp', () => {
let wrapper;
let store;
const findAlert = () => wrapper.findComponent(GlAlert);
+ const findGlButton = () => wrapper.findComponent(GlButton);
+ const findGlModal = () => wrapper.findComponent(GlModal);
const findHeader = () => wrapper.findByTestId('new-jira-connect-ui-heading');
const findHeaderText = () => findHeader().text();
@@ -44,6 +45,33 @@ describe('JiraConnectApp', () => {
expect(findHeaderText()).toBe('Linked namespaces');
});
+ describe('when user is not logged in', () => {
+ beforeEach(() => {
+ createComponent({
+ provide: {
+ glFeatures: { newJiraConnectUi: true },
+ usersPath: '/users',
+ },
+ });
+ });
+
+ it('renders "Sign in" button', () => {
+ expect(findGlButton().text()).toBe('Sign in to add namespaces');
+ expect(findGlModal().exists()).toBe(false);
+ });
+ });
+
+ describe('when user is logged in', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders "Add" button and modal', () => {
+ expect(findGlButton().text()).toBe('Add namespace');
+ expect(findGlModal().exists()).toBe(true);
+ });
+ });
+
describe('newJiraConnectUi is false', () => {
it('does not render new UI', () => {
createComponent({
diff --git a/spec/frontend/jira_connect/components/groups_list_item_spec.js b/spec/frontend/jira_connect/components/groups_list_item_spec.js
index 77577c53cf4..bb247534aca 100644
--- a/spec/frontend/jira_connect/components/groups_list_item_spec.js
+++ b/spec/frontend/jira_connect/components/groups_list_item_spec.js
@@ -1,27 +1,37 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlAvatar } from '@gitlab/ui';
+import { GlAvatar, GlButton } from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import { mockGroup1 } from '../mock_data';
+import waitForPromises from 'helpers/wait_for_promises';
+import * as JiraConnectApi from '~/jira_connect/api';
import GroupsListItem from '~/jira_connect/components/groups_list_item.vue';
+import { mockGroup1 } from '../mock_data';
describe('GroupsListItem', () => {
let wrapper;
+ const mockSubscriptionPath = 'subscriptionPath';
- const createComponent = () => {
+ const reloadSpy = jest.fn();
+
+ global.AP = {
+ navigator: {
+ reload: reloadSpy,
+ },
+ };
+
+ const createComponent = ({ mountFn = shallowMount } = {}) => {
wrapper = extendedWrapper(
- shallowMount(GroupsListItem, {
+ mountFn(GroupsListItem, {
propsData: {
group: mockGroup1,
},
+ provide: {
+ subscriptionsPath: mockSubscriptionPath,
+ },
}),
);
};
- beforeEach(() => {
- createComponent();
- });
-
afterEach(() => {
wrapper.destroy();
wrapper = null;
@@ -30,17 +40,82 @@ describe('GroupsListItem', () => {
const findGlAvatar = () => wrapper.find(GlAvatar);
const findGroupName = () => wrapper.findByTestId('group-list-item-name');
const findGroupDescription = () => wrapper.findByTestId('group-list-item-description');
+ const findLinkButton = () => wrapper.find(GlButton);
+ const clickLinkButton = () => findLinkButton().trigger('click');
- it('renders group avatar', () => {
- expect(findGlAvatar().exists()).toBe(true);
- expect(findGlAvatar().props('src')).toBe(mockGroup1.avatar_url);
- });
+ describe('template', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders group avatar', () => {
+ expect(findGlAvatar().exists()).toBe(true);
+ expect(findGlAvatar().props('src')).toBe(mockGroup1.avatar_url);
+ });
+
+ it('renders group name', () => {
+ expect(findGroupName().text()).toBe(mockGroup1.full_name);
+ });
- it('renders group name', () => {
- expect(findGroupName().text()).toBe(mockGroup1.full_name);
+ it('renders group description', () => {
+ expect(findGroupDescription().text()).toBe(mockGroup1.description);
+ });
+
+ it('renders Link button', () => {
+ expect(findLinkButton().exists()).toBe(true);
+ expect(findLinkButton().text()).toBe('Link');
+ });
});
- it('renders group description', () => {
- expect(findGroupDescription().text()).toBe(mockGroup1.description);
+ describe('on Link button click', () => {
+ let addSubscriptionSpy;
+
+ beforeEach(() => {
+ createComponent({ mountFn: mount });
+
+ addSubscriptionSpy = jest.spyOn(JiraConnectApi, 'addSubscription').mockResolvedValue();
+ });
+
+ it('sets button to loading and sends request', async () => {
+ expect(findLinkButton().props('loading')).toBe(false);
+
+ clickLinkButton();
+
+ await wrapper.vm.$nextTick();
+
+ expect(findLinkButton().props('loading')).toBe(true);
+
+ expect(addSubscriptionSpy).toHaveBeenCalledWith(mockSubscriptionPath, mockGroup1.full_path);
+ });
+
+ describe('when request is successful', () => {
+ it('reloads the page', async () => {
+ clickLinkButton();
+
+ await waitForPromises();
+
+ expect(reloadSpy).toHaveBeenCalled();
+ });
+ });
+
+ describe('when request has errors', () => {
+ const mockErrorMessage = 'error message';
+ const mockError = { response: { data: { error: mockErrorMessage } } };
+
+ beforeEach(() => {
+ addSubscriptionSpy = jest
+ .spyOn(JiraConnectApi, 'addSubscription')
+ .mockRejectedValue(mockError);
+ });
+
+ it('emits `error` event', async () => {
+ clickLinkButton();
+
+ await waitForPromises();
+
+ expect(reloadSpy).not.toHaveBeenCalled();
+ expect(wrapper.emitted('error')[0][0]).toBe(mockErrorMessage);
+ });
+ });
});
});
diff --git a/spec/frontend/jira_connect/components/groups_list_spec.js b/spec/frontend/jira_connect/components/groups_list_spec.js
index 94f158e6344..5c645eccc0e 100644
--- a/spec/frontend/jira_connect/components/groups_list_spec.js
+++ b/spec/frontend/jira_connect/components/groups_list_spec.js
@@ -1,5 +1,5 @@
+import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { GlLoadingIcon } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import { fetchGroups } from '~/jira_connect/api';
@@ -28,6 +28,7 @@ describe('GroupsList', () => {
wrapper = null;
});
+ const findGlAlert = () => wrapper.find(GlAlert);
const findGlLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findAllItems = () => wrapper.findAll(GroupsListItem);
const findFirstItem = () => findAllItems().at(0);
@@ -45,6 +46,18 @@ describe('GroupsList', () => {
});
});
+ describe('error fetching groups', () => {
+ it('renders error message', async () => {
+ fetchGroups.mockRejectedValue();
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findGlAlert().exists()).toBe(true);
+ expect(findGlAlert().text()).toBe('Failed to load namespaces. Please try again.');
+ });
+ });
+
describe('no groups returned', () => {
it('renders empty state', async () => {
fetchGroups.mockResolvedValue(mockEmptyResponse);
@@ -57,15 +70,28 @@ describe('GroupsList', () => {
});
describe('with groups returned', () => {
- it('renders groups list', async () => {
+ beforeEach(async () => {
fetchGroups.mockResolvedValue({ data: [mockGroup1, mockGroup2] });
createComponent();
await waitForPromises();
+ });
+ it('renders groups list', () => {
expect(findAllItems().length).toBe(2);
expect(findFirstItem().props('group')).toBe(mockGroup1);
expect(findSecondItem().props('group')).toBe(mockGroup2);
});
+
+ it('shows error message on $emit from item', async () => {
+ const errorMessage = 'error message';
+
+ findFirstItem().vm.$emit('error', errorMessage);
+
+ await wrapper.vm.$nextTick();
+
+ expect(findGlAlert().exists()).toBe(true);
+ expect(findGlAlert().text()).toContain(errorMessage);
+ });
});
});
diff --git a/spec/frontend/jira_connect/index_spec.js b/spec/frontend/jira_connect/index_spec.js
new file mode 100644
index 00000000000..eb54fe6476f
--- /dev/null
+++ b/spec/frontend/jira_connect/index_spec.js
@@ -0,0 +1,56 @@
+import waitForPromises from 'helpers/wait_for_promises';
+import { initJiraConnect } from '~/jira_connect';
+import { removeSubscription } from '~/jira_connect/api';
+
+jest.mock('~/jira_connect/api', () => ({
+ removeSubscription: jest.fn().mockResolvedValue(),
+ getLocation: jest.fn().mockResolvedValue('test/location'),
+}));
+
+describe('initJiraConnect', () => {
+ window.AP = {
+ navigator: {
+ reload: jest.fn(),
+ },
+ };
+
+ beforeEach(async () => {
+ setFixtures(`
+ <a class="js-jira-connect-sign-in" href="https://gitlab.com">Sign In</a>
+ <a class="js-jira-connect-sign-in" href="https://gitlab.com">Another Sign In</a>
+
+ <a href="https://gitlab.com/sub1" class="js-jira-connect-remove-subscription">Remove</a>
+ <a href="https://gitlab.com/sub2" class="js-jira-connect-remove-subscription">Remove</a>
+ <a href="https://gitlab.com/sub3" class="js-jira-connect-remove-subscription">Remove</a>
+ `);
+
+ await initJiraConnect();
+ });
+
+ describe('Sign in links', () => {
+ it('have `return_to` query parameter', () => {
+ Array.from(document.querySelectorAll('.js-jira-connect-sign-in')).forEach((el) => {
+ expect(el.href).toContain('return_to=test/location');
+ });
+ });
+ });
+
+ describe('`remove subscription` buttons', () => {
+ describe('on click', () => {
+ it('calls `removeSubscription`', () => {
+ Array.from(document.querySelectorAll('.js-jira-connect-remove-subscription')).forEach(
+ (removeSubscriptionButton) => {
+ removeSubscriptionButton.dispatchEvent(new Event('click'));
+
+ waitForPromises();
+
+ expect(removeSubscription).toHaveBeenCalledWith(removeSubscriptionButton.href);
+ expect(removeSubscription).toHaveBeenCalledTimes(1);
+
+ removeSubscription.mockClear();
+ },
+ );
+ });
+ });
+ });
+});
diff --git a/spec/frontend/jira_connect/mock_data.js b/spec/frontend/jira_connect/mock_data.js
index 31565912489..22255fabc3d 100644
--- a/spec/frontend/jira_connect/mock_data.js
+++ b/spec/frontend/jira_connect/mock_data.js
@@ -3,6 +3,7 @@ export const mockGroup1 = {
avatar_url: 'avatar.png',
name: 'Gitlab Org',
full_name: 'Gitlab Org',
+ full_path: 'gitlab-org',
description: 'Open source software to collaborate on code',
};
@@ -11,5 +12,6 @@ export const mockGroup2 = {
avatar_url: 'avatar.png',
name: 'Gitlab Com',
full_name: 'Gitlab Com',
+ full_path: 'gitlab-com',
description: 'For GitLab company related projects',
};
diff --git a/spec/frontend/jira_import/components/jira_import_form_spec.js b/spec/frontend/jira_import/components/jira_import_form_spec.js
index 00fb8f5435e..7a550d85204 100644
--- a/spec/frontend/jira_import/components/jira_import_form_spec.js
+++ b/spec/frontend/jira_import/components/jira_import_form_spec.js
@@ -1,11 +1,21 @@
-import { GlAlert, GlButton, GlDropdown, GlFormSelect, GlLabel, GlTable } from '@gitlab/ui';
+import {
+ GlAlert,
+ GlButton,
+ GlDropdown,
+ GlDropdownItem,
+ GlFormSelect,
+ GlLabel,
+ GlSearchBoxByType,
+ GlTable,
+} from '@gitlab/ui';
import { getByRole } from '@testing-library/dom';
import { mount, shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
import JiraImportForm from '~/jira_import/components/jira_import_form.vue';
import getJiraUserMappingMutation from '~/jira_import/queries/get_jira_user_mapping.mutation.graphql';
import initiateJiraImportMutation from '~/jira_import/queries/initiate_jira_import.mutation.graphql';
+import searchProjectMembersQuery from '~/jira_import/queries/search_project_members.query.graphql';
+import axios from '~/lib/utils/axios_utils';
import {
imports,
issuesPath,
@@ -19,6 +29,7 @@ import {
describe('JiraImportForm', () => {
let axiosMock;
let mutateSpy;
+ let querySpy;
let wrapper;
const currentUsername = 'mrgitlab';
@@ -72,6 +83,7 @@ describe('JiraImportForm', () => {
$apollo: {
loading,
mutate,
+ query: querySpy,
},
},
currentUsername,
@@ -79,19 +91,21 @@ describe('JiraImportForm', () => {
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
- mutateSpy = jest.fn(() =>
- Promise.resolve({
- data: {
- jiraImportStart: { errors: [] },
- jiraImportUsers: { jiraUsers: [], errors: [] },
- },
- }),
- );
+ mutateSpy = jest.fn().mockResolvedValue({
+ data: {
+ jiraImportStart: { errors: [] },
+ jiraImportUsers: { jiraUsers: [], errors: [] },
+ },
+ });
+ querySpy = jest.fn().mockResolvedValue({
+ data: { project: { projectMembers: { nodes: [] } } },
+ });
});
afterEach(() => {
axiosMock.restore();
mutateSpy.mockRestore();
+ querySpy.mockRestore();
wrapper.destroy();
wrapper = null;
});
@@ -236,6 +250,53 @@ describe('JiraImportForm', () => {
});
});
+ describe('member search', () => {
+ describe('when searching for a member', () => {
+ beforeEach(() => {
+ querySpy = jest.fn().mockResolvedValue({
+ data: {
+ project: {
+ projectMembers: {
+ nodes: [
+ {
+ user: {
+ id: 7,
+ name: 'Frederic Chopin',
+ username: 'fchopin',
+ },
+ },
+ ],
+ },
+ },
+ },
+ });
+
+ wrapper = mountComponent({ mountFunction: mount });
+
+ wrapper.find(GlSearchBoxByType).vm.$emit('input', 'fred');
+ });
+
+ it('makes a GraphQL call', () => {
+ const queryArgument = {
+ query: searchProjectMembersQuery,
+ variables: {
+ fullPath: projectPath,
+ search: 'fred',
+ },
+ };
+
+ expect(querySpy).toHaveBeenCalledWith(expect.objectContaining(queryArgument));
+ });
+
+ it('updates the user list', () => {
+ expect(getUserDropdown().findAll(GlDropdownItem)).toHaveLength(1);
+ expect(getUserDropdown().find(GlDropdownItem).text()).toContain(
+ 'fchopin (Frederic Chopin)',
+ );
+ });
+ });
+ });
+
describe('buttons', () => {
describe('"Continue" button', () => {
it('is shown', () => {
diff --git a/spec/frontend/jira_import/mock_data.js b/spec/frontend/jira_import/mock_data.js
index 51dd939283e..8dc8ce08f49 100644
--- a/spec/frontend/jira_import/mock_data.js
+++ b/spec/frontend/jira_import/mock_data.js
@@ -1,6 +1,6 @@
import getJiraImportDetailsQuery from '~/jira_import/queries/get_jira_import_details.query.graphql';
-import { IMPORT_STATE } from '~/jira_import/utils/jira_import_utils';
import { userMappingsPageSize } from '~/jira_import/utils/constants';
+import { IMPORT_STATE } from '~/jira_import/utils/jira_import_utils';
export const fullPath = 'gitlab-org/gitlab-test';
diff --git a/spec/frontend/jira_import/utils/jira_import_utils_spec.js b/spec/frontend/jira_import/utils/jira_import_utils_spec.js
index 0992c9e8d16..9696d95f8c4 100644
--- a/spec/frontend/jira_import/utils/jira_import_utils_spec.js
+++ b/spec/frontend/jira_import/utils/jira_import_utils_spec.js
@@ -1,4 +1,5 @@
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
+import { JIRA_IMPORT_SUCCESS_ALERT_HIDE_MAP_KEY } from '~/issues_list/constants';
import {
calculateJiraImportLabel,
extractJiraProjectsOptions,
@@ -8,7 +9,6 @@ import {
setFinishedAlertHideMap,
shouldShowFinishedAlert,
} from '~/jira_import/utils/jira_import_utils';
-import { JIRA_IMPORT_SUCCESS_ALERT_HIDE_MAP_KEY } from '~/issues_list/constants';
useLocalStorageSpy();
diff --git a/spec/frontend/jobs/components/artifacts_block_spec.js b/spec/frontend/jobs/components/artifacts_block_spec.js
index 134463c6763..0c7c0a6c311 100644
--- a/spec/frontend/jobs/components/artifacts_block_spec.js
+++ b/spec/frontend/jobs/components/artifacts_block_spec.js
@@ -1,7 +1,7 @@
import { mount } from '@vue/test-utils';
import { trimText } from 'helpers/text_helper';
-import { getTimeago } from '~/lib/utils/datetime_utility';
import ArtifactsBlock from '~/jobs/components/artifacts_block.vue';
+import { getTimeago } from '~/lib/utils/datetime_utility';
describe('Artifacts block', () => {
let wrapper;
diff --git a/spec/frontend/jobs/components/erased_block_spec.js b/spec/frontend/jobs/components/erased_block_spec.js
index b3e1d28eb16..057df20ccc2 100644
--- a/spec/frontend/jobs/components/erased_block_spec.js
+++ b/spec/frontend/jobs/components/erased_block_spec.js
@@ -1,7 +1,7 @@
-import { mount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
-import { getTimeago } from '~/lib/utils/datetime_utility';
+import { mount } from '@vue/test-utils';
import ErasedBlock from '~/jobs/components/erased_block.vue';
+import { getTimeago } from '~/lib/utils/datetime_utility';
describe('Erased block', () => {
let wrapper;
@@ -10,6 +10,8 @@ describe('Erased block', () => {
const timeago = getTimeago();
const formattedDate = timeago.format(erasedAt);
+ const findLink = () => wrapper.find(GlLink);
+
const createComponent = (props) => {
wrapper = mount(ErasedBlock, {
propsData: props,
@@ -32,7 +34,7 @@ describe('Erased block', () => {
});
it('renders username and link', () => {
- expect(wrapper.find(GlLink).attributes('href')).toEqual('gitlab.com/root');
+ expect(findLink().attributes('href')).toEqual('gitlab.com/root');
expect(wrapper.text().trim()).toContain('Job has been erased by');
expect(wrapper.text().trim()).toContain('root');
diff --git a/spec/frontend/jobs/components/job_app_spec.js b/spec/frontend/jobs/components/job_app_spec.js
index 657687b5e2a..2974e91e46d 100644
--- a/spec/frontend/jobs/components/job_app_spec.js
+++ b/spec/frontend/jobs/components/job_app_spec.js
@@ -1,18 +1,18 @@
-import Vuex from 'vuex';
-import { mount, createLocalVue } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
+import { mount, createLocalVue } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import Vuex from 'vuex';
import { getJSONFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
-import axios from '~/lib/utils/axios_utils';
+import EmptyState from '~/jobs/components/empty_state.vue';
+import EnvironmentsBlock from '~/jobs/components/environments_block.vue';
+import ErasedBlock from '~/jobs/components/erased_block.vue';
import JobApp from '~/jobs/components/job_app.vue';
import Sidebar from '~/jobs/components/sidebar.vue';
import StuckBlock from '~/jobs/components/stuck_block.vue';
import UnmetPrerequisitesBlock from '~/jobs/components/unmet_prerequisites_block.vue';
-import EnvironmentsBlock from '~/jobs/components/environments_block.vue';
-import ErasedBlock from '~/jobs/components/erased_block.vue';
-import EmptyState from '~/jobs/components/empty_state.vue';
import createStore from '~/jobs/store';
+import axios from '~/lib/utils/axios_utils';
import job from '../mock_data';
describe('Job App', () => {
@@ -34,7 +34,6 @@ describe('Job App', () => {
const props = {
artifactHelpUrl: 'help/artifact',
- runnerHelpUrl: 'help/runner',
deploymentHelpUrl: 'help/deployment',
runnerSettingsUrl: 'settings/ci-cd/runners',
variablesSettingsUrl: 'settings/ci-cd/variables',
diff --git a/spec/frontend/jobs/components/job_container_item_spec.js b/spec/frontend/jobs/components/job_container_item_spec.js
index af7ce100d83..36038b69e64 100644
--- a/spec/frontend/jobs/components/job_container_item_spec.js
+++ b/spec/frontend/jobs/components/job_container_item_spec.js
@@ -1,78 +1,80 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
+import { GlIcon, GlLink } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import JobContainerItem from '~/jobs/components/job_container_item.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import job from '../mock_data';
describe('JobContainerItem', () => {
+ let wrapper;
const delayedJobFixture = getJSONFixture('jobs/delayed.json');
- const Component = Vue.extend(JobContainerItem);
- let vm;
+
+ const findCiIconComponent = () => wrapper.findComponent(CiIcon);
+ const findGlIconComponent = () => wrapper.findComponent(GlIcon);
+
+ function createComponent(jobData = {}, props = { isActive: false, retried: false }) {
+ wrapper = shallowMount(JobContainerItem, {
+ propsData: {
+ job: {
+ ...jobData,
+ retried: props.retried,
+ },
+ isActive: props.isActive,
+ },
+ });
+ }
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
+ wrapper = null;
});
- const sharedTests = () => {
+ describe('when a job is not active and not retried', () => {
+ beforeEach(() => {
+ createComponent(job);
+ });
+
it('displays a status icon', () => {
- expect(vm.$el).toHaveSpriteIcon(job.status.icon);
+ const ciIcon = findCiIconComponent();
+
+ expect(ciIcon.props('status')).toBe(job.status);
});
it('displays the job name', () => {
- expect(vm.$el.innerText).toContain(job.name);
+ expect(wrapper.text()).toContain(job.name);
});
it('displays a link to the job', () => {
- const link = vm.$el.querySelector('.js-job-link');
+ const link = wrapper.findComponent(GlLink);
- expect(link.href).toBe(job.status.details_path);
+ expect(link.attributes('href')).toBe(job.status.details_path);
});
- };
-
- describe('when a job is not active and not retied', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- job,
- isActive: false,
- });
- });
-
- sharedTests();
});
describe('when a job is active', () => {
beforeEach(() => {
- vm = mountComponent(Component, {
- job,
- isActive: true,
- });
+ createComponent(job, { isActive: true });
});
- sharedTests();
+ it('displays an arrow sprite icon', () => {
+ const icon = findGlIconComponent();
- it('displays an arrow', () => {
- expect(vm.$el).toHaveSpriteIcon('arrow-right');
+ expect(icon.props('name')).toBe('arrow-right');
});
});
describe('when a job is retried', () => {
beforeEach(() => {
- vm = mountComponent(Component, {
- job: {
- ...job,
- retried: true,
- },
- isActive: false,
- });
+ createComponent(job, { isActive: false, retried: true });
});
- sharedTests();
+ it('displays a retry icon', () => {
+ const icon = findGlIconComponent();
- it('displays an icon', () => {
- expect(vm.$el).toHaveSpriteIcon('retry');
+ expect(icon.props('name')).toBe('retry');
});
});
- describe('for delayed job', () => {
+ describe('for a delayed job', () => {
beforeEach(() => {
const remainingMilliseconds = 1337000;
jest
@@ -80,22 +82,16 @@ describe('JobContainerItem', () => {
.mockImplementation(
() => new Date(delayedJobFixture.scheduled_at).getTime() - remainingMilliseconds,
);
+
+ createComponent(delayedJobFixture);
});
- it('displays remaining time in tooltip', (done) => {
- vm = mountComponent(Component, {
- job: delayedJobFixture,
- isActive: false,
- });
-
- Vue.nextTick()
- .then(() => {
- expect(vm.$el.querySelector('.js-job-link').getAttribute('title')).toEqual(
- 'delayed job - delayed manual action (00:22:17)',
- );
- })
- .then(done)
- .catch(done.fail);
+ it('displays remaining time in tooltip', async () => {
+ await wrapper.vm.$nextTick();
+
+ const link = wrapper.findComponent(GlLink);
+
+ expect(link.attributes('title')).toMatch('delayed job - delayed manual action (00:22:17)');
});
});
});
diff --git a/spec/frontend/jobs/components/job_sidebar_details_container_spec.js b/spec/frontend/jobs/components/job_sidebar_details_container_spec.js
index bc0d455c309..2b56bd2d558 100644
--- a/spec/frontend/jobs/components/job_sidebar_details_container_spec.js
+++ b/spec/frontend/jobs/components/job_sidebar_details_container_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import SidebarJobDetailsContainer from '~/jobs/components/sidebar_job_details_container.vue';
import DetailRow from '~/jobs/components/sidebar_detail_row.vue';
+import SidebarJobDetailsContainer from '~/jobs/components/sidebar_job_details_container.vue';
import createStore from '~/jobs/store';
import job from '../mock_data';
@@ -116,14 +116,5 @@ describe('Job Sidebar Details Container', () => {
expect(findJobTimeout().exists()).toBe(false);
});
-
- it('should pass the help URL', async () => {
- const helpUrl = 'fakeUrl';
- const props = { runnerHelpUrl: helpUrl };
- createWrapper({ props });
- await store.dispatch('receiveJobSuccess', { metadata: { timeout_human_readable } });
-
- expect(findJobTimeout().props('helpUrl')).toBe(helpUrl);
- });
});
});
diff --git a/spec/frontend/jobs/components/job_sidebar_retry_button_spec.js b/spec/frontend/jobs/components/job_sidebar_retry_button_spec.js
index 4bf697ab7cc..8fc5b071e54 100644
--- a/spec/frontend/jobs/components/job_sidebar_retry_button_spec.js
+++ b/spec/frontend/jobs/components/job_sidebar_retry_button_spec.js
@@ -1,8 +1,8 @@
import { GlButton, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import job from '../mock_data';
import JobsSidebarRetryButton from '~/jobs/components/job_sidebar_retry_button.vue';
import createStore from '~/jobs/store';
+import job from '../mock_data';
describe('Job Sidebar Retry Button', () => {
let store;
diff --git a/spec/frontend/jobs/components/log/line_header_spec.js b/spec/frontend/jobs/components/log/line_header_spec.js
index bb90949b1f4..9763e2f437b 100644
--- a/spec/frontend/jobs/components/log/line_header_spec.js
+++ b/spec/frontend/jobs/components/log/line_header_spec.js
@@ -1,7 +1,7 @@
import { mount } from '@vue/test-utils';
+import DurationBadge from '~/jobs/components/log/duration_badge.vue';
import LineHeader from '~/jobs/components/log/line_header.vue';
import LineNumber from '~/jobs/components/log/line_number.vue';
-import DurationBadge from '~/jobs/components/log/duration_badge.vue';
describe('Job Log Header Line', () => {
let wrapper;
diff --git a/spec/frontend/jobs/components/log/log_spec.js b/spec/frontend/jobs/components/log/log_spec.js
index f662ffa1780..b7aff1f3e3b 100644
--- a/spec/frontend/jobs/components/log/log_spec.js
+++ b/spec/frontend/jobs/components/log/log_spec.js
@@ -1,7 +1,7 @@
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import { logLinesParser } from '~/jobs/store/utils';
import Log from '~/jobs/components/log/log.vue';
+import { logLinesParser } from '~/jobs/store/utils';
import { jobLog } from './mock_data';
describe('Job Log', () => {
diff --git a/spec/frontend/jobs/components/manual_variables_form_spec.js b/spec/frontend/jobs/components/manual_variables_form_spec.js
index f6c37407e2b..7172a319876 100644
--- a/spec/frontend/jobs/components/manual_variables_form_spec.js
+++ b/spec/frontend/jobs/components/manual_variables_form_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
import Form from '~/jobs/components/manual_variables_form.vue';
const localVue = createLocalVue();
diff --git a/spec/frontend/jobs/components/sidebar_detail_row_spec.js b/spec/frontend/jobs/components/sidebar_detail_row_spec.js
index 42d11266dad..bae4d6cf837 100644
--- a/spec/frontend/jobs/components/sidebar_detail_row_spec.js
+++ b/spec/frontend/jobs/components/sidebar_detail_row_spec.js
@@ -1,61 +1,55 @@
-import Vue from 'vue';
-import sidebarDetailRow from '~/jobs/components/sidebar_detail_row.vue';
+import { GlLink } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import SidebarDetailRow from '~/jobs/components/sidebar_detail_row.vue';
describe('Sidebar detail row', () => {
- let SidebarDetailRow;
- let vm;
+ let wrapper;
- beforeEach(() => {
- SidebarDetailRow = Vue.extend(sidebarDetailRow);
- });
+ const title = 'this is the title';
+ const value = 'this is the value';
+ const helpUrl = '/help/ci/runners/README.html';
- afterEach(() => {
- vm.$destroy();
- });
+ const findHelpLink = () => wrapper.findComponent(GlLink);
- it('should render no title', () => {
- vm = new SidebarDetailRow({
+ const createComponent = (props) => {
+ wrapper = shallowMount(SidebarDetailRow, {
propsData: {
- value: 'this is the value',
+ ...props,
},
- }).$mount();
+ });
+ };
- expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toEqual('this is the value');
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
});
- beforeEach(() => {
- vm = new SidebarDetailRow({
- propsData: {
- title: 'this is the title',
- value: 'this is the value',
- },
- }).$mount();
- });
+ describe('with title/value and without helpUrl', () => {
+ beforeEach(() => {
+ createComponent({ title, value });
+ });
- it('should render provided title and value', () => {
- expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toEqual(
- 'this is the title: this is the value',
- );
- });
+ it('should render the provided title and value', () => {
+ expect(wrapper.text()).toBe(`${title}: ${value}`);
+ });
- describe('when helpUrl not provided', () => {
- it('should not render help', () => {
- expect(vm.$el.querySelector('.help-button')).toBeNull();
+ it('should not render the help link', () => {
+ expect(findHelpLink().exists()).toBe(false);
});
});
describe('when helpUrl provided', () => {
beforeEach(() => {
- vm = new SidebarDetailRow({
- propsData: {
- helpUrl: 'help url',
- value: 'foo',
- },
- }).$mount();
+ createComponent({
+ helpUrl,
+ title,
+ value,
+ });
});
- it('should render help', () => {
- expect(vm.$el.querySelector('.help-button a').getAttribute('href')).toEqual('help url');
+ it('should render the help link', () => {
+ expect(findHelpLink().exists()).toBe(true);
+ expect(findHelpLink().attributes('href')).toBe(helpUrl);
});
});
});
diff --git a/spec/frontend/jobs/components/sidebar_spec.js b/spec/frontend/jobs/components/sidebar_spec.js
index 22d555ffec7..5a2e699137d 100644
--- a/spec/frontend/jobs/components/sidebar_spec.js
+++ b/spec/frontend/jobs/components/sidebar_spec.js
@@ -1,10 +1,10 @@
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import Sidebar, { forwardDeploymentFailureModalId } from '~/jobs/components/sidebar.vue';
-import StagesDropdown from '~/jobs/components/stages_dropdown.vue';
-import JobsContainer from '~/jobs/components/jobs_container.vue';
import JobRetryForwardDeploymentModal from '~/jobs/components/job_retry_forward_deployment_modal.vue';
import JobRetryButton from '~/jobs/components/job_sidebar_retry_button.vue';
+import JobsContainer from '~/jobs/components/jobs_container.vue';
+import Sidebar, { forwardDeploymentFailureModalId } from '~/jobs/components/sidebar.vue';
+import StagesDropdown from '~/jobs/components/stages_dropdown.vue';
import createStore from '~/jobs/store';
import job, { jobsInStage } from '../mock_data';
diff --git a/spec/frontend/jobs/components/trigger_block_spec.js b/spec/frontend/jobs/components/trigger_block_spec.js
index 16ea276ee4a..e0eb873dc2f 100644
--- a/spec/frontend/jobs/components/trigger_block_spec.js
+++ b/spec/frontend/jobs/components/trigger_block_spec.js
@@ -1,100 +1,86 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import component from '~/jobs/components/trigger_block.vue';
+import { GlButton, GlTable } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import TriggerBlock from '~/jobs/components/trigger_block.vue';
describe('Trigger block', () => {
- const Component = Vue.extend(component);
- let vm;
+ let wrapper;
+
+ const findRevealButton = () => wrapper.find(GlButton);
+ const findVariableTable = () => wrapper.find(GlTable);
+ const findShortToken = () => wrapper.find('[data-testid="trigger-short-token"]');
+ const findVariableValue = (index) =>
+ wrapper.findAll('[data-testid="trigger-build-value"]').at(index);
+ const findVariableKey = (index) => wrapper.findAll('[data-testid="trigger-build-key"]').at(index);
+
+ const createComponent = (props) => {
+ wrapper = mount(TriggerBlock, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
+ wrapper = null;
});
- describe('with short token', () => {
+ describe('with short token and no variables', () => {
it('renders short token', () => {
- vm = mountComponent(Component, {
+ createComponent({
trigger: {
short_token: '0a666b2',
+ variables: [],
},
});
- expect(vm.$el.querySelector('.js-short-token').textContent).toContain('0a666b2');
+ expect(findShortToken().text()).toContain('0a666b2');
});
});
- describe('without short token', () => {
+ describe('without variables or short token', () => {
+ beforeEach(() => {
+ createComponent({ trigger: { variables: [] } });
+ });
+
it('does not render short token', () => {
- vm = mountComponent(Component, { trigger: {} });
+ expect(findShortToken().exists()).toBe(false);
+ });
- expect(vm.$el.querySelector('.js-short-token')).toBeNull();
+ it('does not render variables', () => {
+ expect(findRevealButton().exists()).toBe(false);
+ expect(findVariableTable().exists()).toBe(false);
});
});
describe('with variables', () => {
describe('hide/reveal variables', () => {
- it('should toggle variables on click', (done) => {
- vm = mountComponent(Component, {
+ it('should toggle variables on click', async () => {
+ const hiddenValue = '••••••';
+ const gcsVar = { key: 'UPLOAD_TO_GCS', value: 'false', public: false };
+ const s3Var = { key: 'UPLOAD_TO_S3', value: 'true', public: false };
+
+ createComponent({
trigger: {
- short_token: 'bd7e',
- variables: [
- { key: 'UPLOAD_TO_GCS', value: 'false', public: false },
- { key: 'UPLOAD_TO_S3', value: 'true', public: false },
- ],
+ variables: [gcsVar, s3Var],
},
});
- vm.$el.querySelector('.js-reveal-variables').click();
-
- vm.$nextTick()
- .then(() => {
- expect(vm.$el.querySelector('.js-build-variables')).not.toBeNull();
- expect(vm.$el.querySelector('.js-reveal-variables').textContent.trim()).toEqual(
- 'Hide values',
- );
-
- expect(vm.$el.querySelector('.js-build-variables').textContent).toContain(
- 'UPLOAD_TO_GCS',
- );
+ expect(findRevealButton().text()).toBe('Reveal values');
- expect(vm.$el.querySelector('.js-build-variables').textContent).toContain('false');
- expect(vm.$el.querySelector('.js-build-variables').textContent).toContain(
- 'UPLOAD_TO_S3',
- );
+ expect(findVariableValue(0).text()).toBe(hiddenValue);
+ expect(findVariableValue(1).text()).toBe(hiddenValue);
- expect(vm.$el.querySelector('.js-build-variables').textContent).toContain('true');
+ expect(findVariableKey(0).text()).toBe(gcsVar.key);
+ expect(findVariableKey(1).text()).toBe(s3Var.key);
- vm.$el.querySelector('.js-reveal-variables').click();
- })
- .then(vm.$nextTick)
- .then(() => {
- expect(vm.$el.querySelector('.js-reveal-variables').textContent.trim()).toEqual(
- 'Reveal values',
- );
+ await findRevealButton().trigger('click');
- expect(vm.$el.querySelector('.js-build-variables').textContent).toContain(
- 'UPLOAD_TO_GCS',
- );
+ expect(findRevealButton().text()).toBe('Hide values');
- expect(vm.$el.querySelector('.js-build-value').textContent).toContain('••••••');
-
- expect(vm.$el.querySelector('.js-build-variables').textContent).toContain(
- 'UPLOAD_TO_S3',
- );
-
- expect(vm.$el.querySelector('.js-build-value').textContent).toContain('••••••');
- })
- .then(done)
- .catch(done.fail);
+ expect(findVariableValue(0).text()).toBe(gcsVar.value);
+ expect(findVariableValue(1).text()).toBe(s3Var.value);
});
});
});
-
- describe('without variables', () => {
- it('does not render variables', () => {
- vm = mountComponent(Component, { trigger: {} });
-
- expect(vm.$el.querySelector('.js-reveal-variables')).toBeNull();
- expect(vm.$el.querySelector('.js-build-variables')).toBeNull();
- });
- });
});
diff --git a/spec/frontend/jobs/components/unmet_prerequisites_block_spec.js b/spec/frontend/jobs/components/unmet_prerequisites_block_spec.js
index 9092d3f8163..aeb85694e60 100644
--- a/spec/frontend/jobs/components/unmet_prerequisites_block_spec.js
+++ b/spec/frontend/jobs/components/unmet_prerequisites_block_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlAlert, GlLink } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import UnmetPrerequisitesBlock from '~/jobs/components/unmet_prerequisites_block.vue';
describe('Unmet Prerequisites Block Job component', () => {
diff --git a/spec/frontend/jobs/mixins/delayed_job_mixin_spec.js b/spec/frontend/jobs/mixins/delayed_job_mixin_spec.js
index 2175610b7a6..838323df755 100644
--- a/spec/frontend/jobs/mixins/delayed_job_mixin_spec.js
+++ b/spec/frontend/jobs/mixins/delayed_job_mixin_spec.js
@@ -1,46 +1,42 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
+import { shallowMount } from '@vue/test-utils';
import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
describe('DelayedJobMixin', () => {
+ let wrapper;
const delayedJobFixture = getJSONFixture('jobs/delayed.json');
- const dummyComponent = Vue.extend({
- mixins: [delayedJobMixin],
+ const dummyComponent = {
props: {
job: {
type: Object,
required: true,
},
},
- render(createElement) {
- return createElement('div', this.remainingTime);
- },
- });
-
- let vm;
+ mixins: [delayedJobMixin],
+ template: '<div>{{remainingTime}}</div>',
+ };
afterEach(() => {
- vm.$destroy();
- jest.clearAllTimers();
+ wrapper.destroy();
+ wrapper = null;
});
describe('if job is empty object', () => {
beforeEach(() => {
- vm = mountComponent(dummyComponent, {
- job: {},
+ wrapper = shallowMount(dummyComponent, {
+ propsData: {
+ job: {},
+ },
});
});
it('sets remaining time to 00:00:00', () => {
- expect(vm.$el.innerText).toBe('00:00:00');
+ expect(wrapper.text()).toBe('00:00:00');
});
- describe('after mounting', () => {
- beforeEach(() => vm.$nextTick());
+ it('does not update remaining time after mounting', async () => {
+ await wrapper.vm.$nextTick();
- it('does not update remaining time', () => {
- expect(vm.$el.innerText).toBe('00:00:00');
- });
+ expect(wrapper.text()).toBe('00:00:00');
});
});
@@ -48,33 +44,32 @@ describe('DelayedJobMixin', () => {
describe('if job is delayed job', () => {
let remainingTimeInMilliseconds = 42000;
- beforeEach(() => {
+ beforeEach(async () => {
jest
.spyOn(Date, 'now')
.mockImplementation(
() => new Date(delayedJobFixture.scheduled_at).getTime() - remainingTimeInMilliseconds,
);
- vm = mountComponent(dummyComponent, {
- job: delayedJobFixture,
+ wrapper = shallowMount(dummyComponent, {
+ propsData: {
+ job: delayedJobFixture,
+ },
});
- });
- describe('after mounting', () => {
- beforeEach(() => vm.$nextTick());
+ await wrapper.vm.$nextTick();
+ });
- it('sets remaining time', () => {
- expect(vm.$el.innerText).toBe('00:00:42');
- });
+ it('sets remaining time', () => {
+ expect(wrapper.text()).toBe('00:00:42');
+ });
- it('updates remaining time', () => {
- remainingTimeInMilliseconds = 41000;
- jest.advanceTimersByTime(1000);
+ it('updates remaining time', async () => {
+ remainingTimeInMilliseconds = 41000;
+ jest.advanceTimersByTime(1000);
- return vm.$nextTick().then(() => {
- expect(vm.$el.innerText).toBe('00:00:41');
- });
- });
+ await wrapper.vm.$nextTick();
+ expect(wrapper.text()).toBe('00:00:41');
});
});
});
@@ -96,33 +91,32 @@ describe('DelayedJobMixin', () => {
describe('if job is delayed job', () => {
let remainingTimeInMilliseconds = 42000;
- beforeEach(() => {
+ beforeEach(async () => {
jest
.spyOn(Date, 'now')
.mockImplementation(
() => mockGraphQlJob.scheduledAt.getTime() - remainingTimeInMilliseconds,
);
- vm = mountComponent(dummyComponent, {
- job: mockGraphQlJob,
+ wrapper = shallowMount(dummyComponent, {
+ propsData: {
+ job: mockGraphQlJob,
+ },
});
- });
- describe('after mounting', () => {
- beforeEach(() => vm.$nextTick());
+ await wrapper.vm.$nextTick();
+ });
- it('sets remaining time', () => {
- expect(vm.$el.innerText).toBe('00:00:42');
- });
+ it('sets remaining time', () => {
+ expect(wrapper.text()).toBe('00:00:42');
+ });
- it('updates remaining time', () => {
- remainingTimeInMilliseconds = 41000;
- jest.advanceTimersByTime(1000);
+ it('updates remaining time', async () => {
+ remainingTimeInMilliseconds = 41000;
+ jest.advanceTimersByTime(1000);
- return vm.$nextTick().then(() => {
- expect(vm.$el.innerText).toBe('00:00:41');
- });
- });
+ await wrapper.vm.$nextTick();
+ expect(wrapper.text()).toBe('00:00:41');
});
});
});
diff --git a/spec/frontend/jobs/store/actions_spec.js b/spec/frontend/jobs/store/actions_spec.js
index 2d757ce76bf..a29bd15099f 100644
--- a/spec/frontend/jobs/store/actions_spec.js
+++ b/spec/frontend/jobs/store/actions_spec.js
@@ -1,7 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
-import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'helpers/test_constants';
-import axios from '~/lib/utils/axios_utils';
+import testAction from 'helpers/vuex_action_helper';
import {
setJobEndpoint,
setTraceOptions,
@@ -28,8 +27,9 @@ import {
showSidebar,
toggleSidebar,
} from '~/jobs/store/actions';
-import state from '~/jobs/store/state';
import * as types from '~/jobs/store/mutation_types';
+import state from '~/jobs/store/state';
+import axios from '~/lib/utils/axios_utils';
describe('Job State actions', () => {
let mockedState;
diff --git a/spec/frontend/jobs/store/mutations_spec.js b/spec/frontend/jobs/store/mutations_spec.js
index 608abc8f7c4..1c7e45dfb3d 100644
--- a/spec/frontend/jobs/store/mutations_spec.js
+++ b/spec/frontend/jobs/store/mutations_spec.js
@@ -1,6 +1,6 @@
-import state from '~/jobs/store/state';
-import mutations from '~/jobs/store/mutations';
import * as types from '~/jobs/store/mutation_types';
+import mutations from '~/jobs/store/mutations';
+import state from '~/jobs/store/state';
describe('Jobs Store Mutations', () => {
let stateCopy;
diff --git a/spec/frontend/lazy_loader_spec.js b/spec/frontend/lazy_loader_spec.js
index f8ac7568724..3d8b0d9c307 100644
--- a/spec/frontend/lazy_loader_spec.js
+++ b/spec/frontend/lazy_loader_spec.js
@@ -1,6 +1,6 @@
import { noop } from 'lodash';
-import { TEST_HOST } from 'helpers/test_constants';
import { useMockMutationObserver, useMockIntersectionObserver } from 'helpers/mock_dom_observer';
+import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import LazyLoader from '~/lazy_loader';
diff --git a/spec/frontend/lib/utils/ajax_cache_spec.js b/spec/frontend/lib/utils/ajax_cache_spec.js
index 641dd3684fa..d4b95172d18 100644
--- a/spec/frontend/lib/utils/ajax_cache_spec.js
+++ b/spec/frontend/lib/utils/ajax_cache_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
import AjaxCache from '~/lib/utils/ajax_cache';
+import axios from '~/lib/utils/axios_utils';
describe('AjaxCache', () => {
const dummyEndpoint = '/AjaxCache/dummyEndpoint';
diff --git a/spec/frontend/lib/utils/array_utility_spec.js b/spec/frontend/lib/utils/array_utility_spec.js
new file mode 100644
index 00000000000..b95286ff254
--- /dev/null
+++ b/spec/frontend/lib/utils/array_utility_spec.js
@@ -0,0 +1,32 @@
+import * as arrayUtils from '~/lib/utils/array_utility';
+
+describe('array_utility', () => {
+ describe('swapArrayItems', () => {
+ it.each`
+ array | leftIndex | rightIndex | result
+ ${[]} | ${0} | ${0} | ${[]}
+ ${[1]} | ${0} | ${1} | ${[1]}
+ ${[1, 2]} | ${0} | ${0} | ${[1, 2]}
+ ${[1, 2]} | ${0} | ${1} | ${[2, 1]}
+ ${[1, 2]} | ${1} | ${2} | ${[1, 2]}
+ ${[1, 2]} | ${2} | ${1} | ${[1, 2]}
+ ${[1, 2]} | ${1} | ${10} | ${[1, 2]}
+ ${[1, 2]} | ${10} | ${1} | ${[1, 2]}
+ ${[1, 2]} | ${1} | ${-1} | ${[1, 2]}
+ ${[1, 2]} | ${-1} | ${1} | ${[1, 2]}
+ ${[1, 2, 3]} | ${1} | ${1} | ${[1, 2, 3]}
+ ${[1, 2, 3]} | ${0} | ${2} | ${[3, 2, 1]}
+ ${[1, 2, 3, 4]} | ${0} | ${2} | ${[3, 2, 1, 4]}
+ ${[1, 2, 3, 4, 5]} | ${0} | ${4} | ${[5, 2, 3, 4, 1]}
+ ${[1, 2, 3, 4, 5]} | ${1} | ${2} | ${[1, 3, 2, 4, 5]}
+ ${[1, 2, 3, 4, 5]} | ${2} | ${1} | ${[1, 3, 2, 4, 5]}
+ `(
+ 'given $array with index $leftIndex and $rightIndex will return $result',
+ ({ array, leftIndex, rightIndex, result }) => {
+ const actual = arrayUtils.swapArrayItems(array, leftIndex, rightIndex);
+ expect(actual).toEqual(result);
+ expect(actual).not.toBe(array);
+ },
+ );
+ });
+});
diff --git a/spec/frontend/lib/utils/color_utils_spec.js b/spec/frontend/lib/utils/color_utils_spec.js
index 433e9d5a85e..8c846abd77f 100644
--- a/spec/frontend/lib/utils/color_utils_spec.js
+++ b/spec/frontend/lib/utils/color_utils_spec.js
@@ -1,4 +1,4 @@
-import { textColorForBackground, hexToRgb } from '~/lib/utils/color_utils';
+import { textColorForBackground, hexToRgb, validateHexColor } from '~/lib/utils/color_utils';
describe('Color utils', () => {
describe('Converting hex code to rgb', () => {
@@ -32,4 +32,19 @@ describe('Color utils', () => {
expect(textColorForBackground('#000')).toEqual('#FFFFFF');
});
});
+
+ describe('Validate hex color', () => {
+ it.each`
+ color | output
+ ${undefined} | ${null}
+ ${null} | ${null}
+ ${''} | ${null}
+ ${'ABC123'} | ${false}
+ ${'#ZZZ'} | ${false}
+ ${'#FF0'} | ${true}
+ ${'#FF0000'} | ${true}
+ `('returns $output when $color is given', ({ color, output }) => {
+ expect(validateHexColor(color)).toEqual(output);
+ });
+ });
});
diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js
index 90222f0f718..18be88a0b8b 100644
--- a/spec/frontend/lib/utils/common_utils_spec.js
+++ b/spec/frontend/lib/utils/common_utils_spec.js
@@ -1045,4 +1045,12 @@ describe('common_utils', () => {
expect(commonUtils.getDashPath('/some/url')).toEqual(null);
});
});
+
+ describe('convertArrayToCamelCase', () => {
+ it('returns a new array with snake_case string elements converted camelCase', () => {
+ const result = commonUtils.convertArrayToCamelCase(['hello', 'hello_world']);
+
+ expect(result).toEqual(['hello', 'helloWorld']);
+ });
+ });
});
diff --git a/spec/frontend/lib/utils/datetime_utility_spec.js b/spec/frontend/lib/utils/datetime_utility_spec.js
index 66efd43262b..32a24227cbd 100644
--- a/spec/frontend/lib/utils/datetime_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime_utility_spec.js
@@ -1,8 +1,8 @@
import $ from 'jquery';
import timezoneMock from 'timezone-mock';
+import * as datetimeUtility from '~/lib/utils/datetime_utility';
import { __, s__ } from '~/locale';
import '~/commons/bootstrap';
-import * as datetimeUtility from '~/lib/utils/datetime_utility';
describe('Date time utils', () => {
describe('timeFor', () => {
@@ -584,22 +584,6 @@ describe('secondsToMilliseconds', () => {
});
});
-describe('dayAfter', () => {
- const date = new Date('2019-07-16T00:00:00.000Z');
-
- it('returns the following date', () => {
- const nextDay = datetimeUtility.dayAfter(date);
- const expectedNextDate = new Date('2019-07-17T00:00:00.000Z');
-
- expect(nextDay).toStrictEqual(expectedNextDate);
- });
-
- it('does not modifiy the original date', () => {
- datetimeUtility.dayAfter(date);
- expect(date).toStrictEqual(new Date('2019-07-16T00:00:00.000Z'));
- });
-});
-
describe('secondsToDays', () => {
it('converts seconds to days correctly', () => {
expect(datetimeUtility.secondsToDays(0)).toBe(0);
@@ -608,90 +592,214 @@ describe('secondsToDays', () => {
});
});
-describe('nDaysAfter', () => {
- const date = new Date('2019-07-16T00:00:00.000Z');
+describe('date addition/subtraction methods', () => {
+ beforeEach(() => {
+ timezoneMock.register('US/Eastern');
+ });
- it.each`
- numberOfDays | expectedResult
- ${1} | ${new Date('2019-07-17T00:00:00.000Z').valueOf()}
- ${90} | ${new Date('2019-10-14T00:00:00.000Z').valueOf()}
- ${-1} | ${new Date('2019-07-15T00:00:00.000Z').valueOf()}
- ${0} | ${date.valueOf()}
- ${0.9} | ${date.valueOf()}
- `('returns $numberOfDays day(s) after the provided date', ({ numberOfDays, expectedResult }) => {
- expect(datetimeUtility.nDaysAfter(date, numberOfDays)).toBe(expectedResult);
+ afterEach(() => {
+ timezoneMock.unregister();
});
-});
-describe('nDaysBefore', () => {
- const date = new Date('2019-07-16T00:00:00.000Z');
+ describe('dayAfter', () => {
+ const input = '2019-03-10T00:00:00.000Z';
+ const expectedLocalResult = '2019-03-10T23:00:00.000Z';
+ const expectedUTCResult = '2019-03-11T00:00:00.000Z';
+
+ it.each`
+ inputAsString | options | expectedAsString
+ ${input} | ${undefined} | ${expectedLocalResult}
+ ${input} | ${{}} | ${expectedLocalResult}
+ ${input} | ${{ utc: false }} | ${expectedLocalResult}
+ ${input} | ${{ utc: true }} | ${expectedUTCResult}
+ `(
+ 'when the provided date is $inputAsString and the options parameter is $options, returns $expectedAsString',
+ ({ inputAsString, options, expectedAsString }) => {
+ const inputDate = new Date(inputAsString);
+ const actual = datetimeUtility.dayAfter(inputDate, options);
+
+ expect(actual.toISOString()).toBe(expectedAsString);
+ },
+ );
+
+ it('does not modifiy the original date', () => {
+ const inputDate = new Date(input);
+ datetimeUtility.dayAfter(inputDate);
+ expect(inputDate.toISOString()).toBe(input);
+ });
+ });
- it.each`
- numberOfDays | expectedResult
- ${1} | ${new Date('2019-07-15T00:00:00.000Z').valueOf()}
- ${90} | ${new Date('2019-04-17T00:00:00.000Z').valueOf()}
- ${-1} | ${new Date('2019-07-17T00:00:00.000Z').valueOf()}
- ${0} | ${date.valueOf()}
- ${0.9} | ${new Date('2019-07-15T00:00:00.000Z').valueOf()}
- `('returns $numberOfDays day(s) before the provided date', ({ numberOfDays, expectedResult }) => {
- expect(datetimeUtility.nDaysBefore(date, numberOfDays)).toBe(expectedResult);
+ describe('nDaysAfter', () => {
+ const input = '2019-07-16T00:00:00.000Z';
+
+ it.each`
+ inputAsString | numberOfDays | options | expectedAsString
+ ${input} | ${1} | ${undefined} | ${'2019-07-17T00:00:00.000Z'}
+ ${input} | ${-1} | ${undefined} | ${'2019-07-15T00:00:00.000Z'}
+ ${input} | ${0} | ${undefined} | ${'2019-07-16T00:00:00.000Z'}
+ ${input} | ${0.9} | ${undefined} | ${'2019-07-16T00:00:00.000Z'}
+ ${input} | ${120} | ${undefined} | ${'2019-11-13T01:00:00.000Z'}
+ ${input} | ${120} | ${{}} | ${'2019-11-13T01:00:00.000Z'}
+ ${input} | ${120} | ${{ utc: false }} | ${'2019-11-13T01:00:00.000Z'}
+ ${input} | ${120} | ${{ utc: true }} | ${'2019-11-13T00:00:00.000Z'}
+ `(
+ 'when the provided date is $inputAsString, numberOfDays is $numberOfDays, and the options parameter is $options, returns $expectedAsString',
+ ({ inputAsString, numberOfDays, options, expectedAsString }) => {
+ const inputDate = new Date(inputAsString);
+ const actual = datetimeUtility.nDaysAfter(inputDate, numberOfDays, options);
+
+ expect(actual.toISOString()).toBe(expectedAsString);
+ },
+ );
});
-});
-describe('nMonthsAfter', () => {
- // February has 28 days
- const feb2019 = new Date('2019-02-15T00:00:00.000Z');
- // Except in 2020, it had 29 days
- const feb2020 = new Date('2020-02-15T00:00:00.000Z');
- // April has 30 days
- const apr2020 = new Date('2020-04-15T00:00:00.000Z');
- // May has 31 days
- const may2020 = new Date('2020-05-15T00:00:00.000Z');
+ describe('nDaysBefore', () => {
+ const input = '2019-07-16T00:00:00.000Z';
+
+ it.each`
+ inputAsString | numberOfDays | options | expectedAsString
+ ${input} | ${1} | ${undefined} | ${'2019-07-15T00:00:00.000Z'}
+ ${input} | ${-1} | ${undefined} | ${'2019-07-17T00:00:00.000Z'}
+ ${input} | ${0} | ${undefined} | ${'2019-07-16T00:00:00.000Z'}
+ ${input} | ${0.9} | ${undefined} | ${'2019-07-15T00:00:00.000Z'}
+ ${input} | ${180} | ${undefined} | ${'2019-01-17T01:00:00.000Z'}
+ ${input} | ${180} | ${{}} | ${'2019-01-17T01:00:00.000Z'}
+ ${input} | ${180} | ${{ utc: false }} | ${'2019-01-17T01:00:00.000Z'}
+ ${input} | ${180} | ${{ utc: true }} | ${'2019-01-17T00:00:00.000Z'}
+ `(
+ 'when the provided date is $inputAsString, numberOfDays is $numberOfDays, and the options parameter is $options, returns $expectedAsString',
+ ({ inputAsString, numberOfDays, options, expectedAsString }) => {
+ const inputDate = new Date(inputAsString);
+ const actual = datetimeUtility.nDaysBefore(inputDate, numberOfDays, options);
+
+ expect(actual.toISOString()).toBe(expectedAsString);
+ },
+ );
+ });
- it.each`
- date | numberOfMonths | expectedResult
- ${feb2019} | ${1} | ${new Date('2019-03-15T00:00:00.000Z').valueOf()}
- ${feb2020} | ${1} | ${new Date('2020-03-15T00:00:00.000Z').valueOf()}
- ${apr2020} | ${1} | ${new Date('2020-05-15T00:00:00.000Z').valueOf()}
- ${may2020} | ${1} | ${new Date('2020-06-15T00:00:00.000Z').valueOf()}
- ${may2020} | ${12} | ${new Date('2021-05-15T00:00:00.000Z').valueOf()}
- ${may2020} | ${-1} | ${new Date('2020-04-15T00:00:00.000Z').valueOf()}
- ${may2020} | ${0} | ${may2020.valueOf()}
- ${may2020} | ${0.9} | ${may2020.valueOf()}
- `(
- 'returns $numberOfMonths month(s) after the provided date',
- ({ date, numberOfMonths, expectedResult }) => {
- expect(datetimeUtility.nMonthsAfter(date, numberOfMonths)).toBe(expectedResult);
- },
- );
-});
+ describe('nWeeksAfter', () => {
+ const input = '2021-07-16T00:00:00.000Z';
+
+ it.each`
+ inputAsString | numberOfWeeks | options | expectedAsString
+ ${input} | ${1} | ${undefined} | ${'2021-07-23T00:00:00.000Z'}
+ ${input} | ${3} | ${undefined} | ${'2021-08-06T00:00:00.000Z'}
+ ${input} | ${-1} | ${undefined} | ${'2021-07-09T00:00:00.000Z'}
+ ${input} | ${0} | ${undefined} | ${'2021-07-16T00:00:00.000Z'}
+ ${input} | ${0.6} | ${undefined} | ${'2021-07-20T00:00:00.000Z'}
+ ${input} | ${18} | ${undefined} | ${'2021-11-19T01:00:00.000Z'}
+ ${input} | ${18} | ${{}} | ${'2021-11-19T01:00:00.000Z'}
+ ${input} | ${18} | ${{ utc: false }} | ${'2021-11-19T01:00:00.000Z'}
+ ${input} | ${18} | ${{ utc: true }} | ${'2021-11-19T00:00:00.000Z'}
+ `(
+ 'when the provided date is $inputAsString, numberOfWeeks is $numberOfWeeks, and the options parameter is $options, returns $expectedAsString',
+ ({ inputAsString, numberOfWeeks, options, expectedAsString }) => {
+ const inputDate = new Date(inputAsString);
+ const actual = datetimeUtility.nWeeksAfter(inputDate, numberOfWeeks, options);
+
+ expect(actual.toISOString()).toBe(expectedAsString);
+ },
+ );
+ });
-describe('nMonthsBefore', () => {
- // The previous month (February) has 28 days
- const march2019 = new Date('2019-03-15T00:00:00.000Z');
- // Except in 2020, it had 29 days
- const march2020 = new Date('2020-03-15T00:00:00.000Z');
- // The previous month (April) has 30 days
- const may2020 = new Date('2020-05-15T00:00:00.000Z');
- // The previous month (May) has 31 days
- const june2020 = new Date('2020-06-15T00:00:00.000Z');
+ describe('nWeeksBefore', () => {
+ const input = '2021-07-16T00:00:00.000Z';
+
+ it.each`
+ inputAsString | numberOfWeeks | options | expectedAsString
+ ${input} | ${1} | ${undefined} | ${'2021-07-09T00:00:00.000Z'}
+ ${input} | ${3} | ${undefined} | ${'2021-06-25T00:00:00.000Z'}
+ ${input} | ${-1} | ${undefined} | ${'2021-07-23T00:00:00.000Z'}
+ ${input} | ${0} | ${undefined} | ${'2021-07-16T00:00:00.000Z'}
+ ${input} | ${0.6} | ${undefined} | ${'2021-07-11T00:00:00.000Z'}
+ ${input} | ${20} | ${undefined} | ${'2021-02-26T01:00:00.000Z'}
+ ${input} | ${20} | ${{}} | ${'2021-02-26T01:00:00.000Z'}
+ ${input} | ${20} | ${{ utc: false }} | ${'2021-02-26T01:00:00.000Z'}
+ ${input} | ${20} | ${{ utc: true }} | ${'2021-02-26T00:00:00.000Z'}
+ `(
+ 'when the provided date is $inputAsString, numberOfWeeks is $numberOfWeeks, and the options parameter is $options, returns $expectedAsString',
+ ({ inputAsString, numberOfWeeks, options, expectedAsString }) => {
+ const inputDate = new Date(inputAsString);
+ const actual = datetimeUtility.nWeeksBefore(inputDate, numberOfWeeks, options);
+
+ expect(actual.toISOString()).toBe(expectedAsString);
+ },
+ );
+ });
- it.each`
- date | numberOfMonths | expectedResult
- ${march2019} | ${1} | ${new Date('2019-02-15T00:00:00.000Z').valueOf()}
- ${march2020} | ${1} | ${new Date('2020-02-15T00:00:00.000Z').valueOf()}
- ${may2020} | ${1} | ${new Date('2020-04-15T00:00:00.000Z').valueOf()}
- ${june2020} | ${1} | ${new Date('2020-05-15T00:00:00.000Z').valueOf()}
- ${june2020} | ${12} | ${new Date('2019-06-15T00:00:00.000Z').valueOf()}
- ${june2020} | ${-1} | ${new Date('2020-07-15T00:00:00.000Z').valueOf()}
- ${june2020} | ${0} | ${june2020.valueOf()}
- ${june2020} | ${0.9} | ${new Date('2020-05-15T00:00:00.000Z').valueOf()}
- `(
- 'returns $numberOfMonths month(s) before the provided date',
- ({ date, numberOfMonths, expectedResult }) => {
- expect(datetimeUtility.nMonthsBefore(date, numberOfMonths)).toBe(expectedResult);
- },
- );
+ describe('nMonthsAfter', () => {
+ // February has 28 days
+ const feb2019 = '2019-02-15T00:00:00.000Z';
+ // Except in 2020, it had 29 days
+ const feb2020 = '2020-02-15T00:00:00.000Z';
+ // April has 30 days
+ const apr2020 = '2020-04-15T00:00:00.000Z';
+ // May has 31 days
+ const may2020 = '2020-05-15T00:00:00.000Z';
+ // November 1, 2020 was the day Daylight Saving Time ended in 2020 (in the US)
+ const oct2020 = '2020-10-15T00:00:00.000Z';
+
+ it.each`
+ inputAsString | numberOfMonths | options | expectedAsString
+ ${feb2019} | ${1} | ${undefined} | ${'2019-03-14T23:00:00.000Z'}
+ ${feb2020} | ${1} | ${undefined} | ${'2020-03-14T23:00:00.000Z'}
+ ${apr2020} | ${1} | ${undefined} | ${'2020-05-15T00:00:00.000Z'}
+ ${may2020} | ${1} | ${undefined} | ${'2020-06-15T00:00:00.000Z'}
+ ${may2020} | ${12} | ${undefined} | ${'2021-05-15T00:00:00.000Z'}
+ ${may2020} | ${-1} | ${undefined} | ${'2020-04-15T00:00:00.000Z'}
+ ${may2020} | ${0} | ${undefined} | ${may2020}
+ ${may2020} | ${0.9} | ${undefined} | ${may2020}
+ ${oct2020} | ${1} | ${undefined} | ${'2020-11-15T01:00:00.000Z'}
+ ${oct2020} | ${1} | ${{}} | ${'2020-11-15T01:00:00.000Z'}
+ ${oct2020} | ${1} | ${{ utc: false }} | ${'2020-11-15T01:00:00.000Z'}
+ ${oct2020} | ${1} | ${{ utc: true }} | ${'2020-11-15T00:00:00.000Z'}
+ `(
+ 'when the provided date is $inputAsString, numberOfMonths is $numberOfMonths, and the options parameter is $options, returns $expectedAsString',
+ ({ inputAsString, numberOfMonths, options, expectedAsString }) => {
+ const inputDate = new Date(inputAsString);
+ const actual = datetimeUtility.nMonthsAfter(inputDate, numberOfMonths, options);
+
+ expect(actual.toISOString()).toBe(expectedAsString);
+ },
+ );
+ });
+
+ describe('nMonthsBefore', () => {
+ // The previous month (February) has 28 days
+ const march2019 = '2019-03-15T00:00:00.000Z';
+ // Except in 2020, it had 29 days
+ const march2020 = '2020-03-15T00:00:00.000Z';
+ // The previous month (April) has 30 days
+ const may2020 = '2020-05-15T00:00:00.000Z';
+ // The previous month (May) has 31 days
+ const june2020 = '2020-06-15T00:00:00.000Z';
+ // November 1, 2020 was the day Daylight Saving Time ended in 2020 (in the US)
+ const nov2020 = '2020-11-15T00:00:00.000Z';
+
+ it.each`
+ inputAsString | numberOfMonths | options | expectedAsString
+ ${march2019} | ${1} | ${undefined} | ${'2019-02-15T01:00:00.000Z'}
+ ${march2020} | ${1} | ${undefined} | ${'2020-02-15T01:00:00.000Z'}
+ ${may2020} | ${1} | ${undefined} | ${'2020-04-15T00:00:00.000Z'}
+ ${june2020} | ${1} | ${undefined} | ${'2020-05-15T00:00:00.000Z'}
+ ${june2020} | ${12} | ${undefined} | ${'2019-06-15T00:00:00.000Z'}
+ ${june2020} | ${-1} | ${undefined} | ${'2020-07-15T00:00:00.000Z'}
+ ${june2020} | ${0} | ${undefined} | ${june2020}
+ ${june2020} | ${0.9} | ${undefined} | ${'2020-05-15T00:00:00.000Z'}
+ ${nov2020} | ${1} | ${undefined} | ${'2020-10-14T23:00:00.000Z'}
+ ${nov2020} | ${1} | ${{}} | ${'2020-10-14T23:00:00.000Z'}
+ ${nov2020} | ${1} | ${{ utc: false }} | ${'2020-10-14T23:00:00.000Z'}
+ ${nov2020} | ${1} | ${{ utc: true }} | ${'2020-10-15T00:00:00.000Z'}
+ `(
+ 'when the provided date is $inputAsString, numberOfMonths is $numberOfMonths, and the options parameter is $options, returns $expectedAsString',
+ ({ inputAsString, numberOfMonths, options, expectedAsString }) => {
+ const inputDate = new Date(inputAsString);
+ const actual = datetimeUtility.nMonthsBefore(inputDate, numberOfMonths, options);
+
+ expect(actual.toISOString()).toBe(expectedAsString);
+ },
+ );
+ });
});
describe('approximateDuration', () => {
@@ -843,7 +951,7 @@ describe('format24HourTimeStringFromInt', () => {
});
});
-describe('getOverlappingDaysInPeriods', () => {
+describe('getOverlapDateInPeriods', () => {
const start = new Date(2021, 0, 11);
const end = new Date(2021, 0, 13);
@@ -851,14 +959,15 @@ describe('getOverlappingDaysInPeriods', () => {
const givenPeriodLeft = new Date(2021, 0, 11);
const givenPeriodRight = new Date(2021, 0, 14);
- it('returns an overlap object that contains the amount of days overlapping, start date of overlap and end date of overlap', () => {
+ it('returns an overlap object that contains the amount of days overlapping, the amount of hours overlapping, start date of overlap and end date of overlap', () => {
expect(
- datetimeUtility.getOverlappingDaysInPeriods(
+ datetimeUtility.getOverlapDateInPeriods(
{ start, end },
{ start: givenPeriodLeft, end: givenPeriodRight },
),
).toEqual({
daysOverlap: 2,
+ hoursOverlap: 48,
overlapStartDate: givenPeriodLeft.getTime(),
overlapEndDate: end.getTime(),
});
@@ -871,7 +980,7 @@ describe('getOverlappingDaysInPeriods', () => {
it('returns an overlap object that contains a 0 value for days overlapping', () => {
expect(
- datetimeUtility.getOverlappingDaysInPeriods(
+ datetimeUtility.getOverlapDateInPeriods(
{ start, end },
{ start: givenPeriodLeft, end: givenPeriodRight },
),
@@ -886,14 +995,54 @@ describe('getOverlappingDaysInPeriods', () => {
it('throws an exception when the left period contains an invalid date', () => {
expect(() =>
- datetimeUtility.getOverlappingDaysInPeriods({ start, end }, { start: startInvalid, end }),
+ datetimeUtility.getOverlapDateInPeriods({ start, end }, { start: startInvalid, end }),
).toThrow(error);
});
it('throws an exception when the right period contains an invalid date', () => {
expect(() =>
- datetimeUtility.getOverlappingDaysInPeriods({ start, end }, { start, end: endInvalid }),
+ datetimeUtility.getOverlapDateInPeriods({ start, end }, { start, end: endInvalid }),
).toThrow(error);
});
});
});
+
+describe('isToday', () => {
+ const today = new Date();
+ it.each`
+ date | expected | negation
+ ${today} | ${true} | ${'is'}
+ ${new Date('2021-01-21T12:00:00.000Z')} | ${false} | ${'is NOT'}
+ `('returns $expected as $date $negation today', ({ date, expected }) => {
+ expect(datetimeUtility.isToday(date)).toBe(expected);
+ });
+});
+
+describe('getStartOfDay', () => {
+ beforeEach(() => {
+ timezoneMock.register('US/Eastern');
+ });
+
+ afterEach(() => {
+ timezoneMock.unregister();
+ });
+
+ it.each`
+ inputAsString | options | expectedAsString
+ ${'2021-01-29T18:08:23.014Z'} | ${undefined} | ${'2021-01-29T05:00:00.000Z'}
+ ${'2021-01-29T13:08:23.014-05:00'} | ${undefined} | ${'2021-01-29T05:00:00.000Z'}
+ ${'2021-01-30T03:08:23.014+09:00'} | ${undefined} | ${'2021-01-29T05:00:00.000Z'}
+ ${'2021-01-28T18:08:23.014-10:00'} | ${undefined} | ${'2021-01-28T05:00:00.000Z'}
+ ${'2021-01-28T18:08:23.014-10:00'} | ${{}} | ${'2021-01-28T05:00:00.000Z'}
+ ${'2021-01-28T18:08:23.014-10:00'} | ${{ utc: false }} | ${'2021-01-28T05:00:00.000Z'}
+ ${'2021-01-28T18:08:23.014-10:00'} | ${{ utc: true }} | ${'2021-01-29T00:00:00.000Z'}
+ `(
+ 'when the provided date is $inputAsString and the options parameter is $options, returns $expectedAsString',
+ ({ inputAsString, options, expectedAsString }) => {
+ const inputDate = new Date(inputAsString);
+ const actual = datetimeUtility.getStartOfDay(inputDate, options);
+
+ expect(actual.toISOString()).toEqual(expectedAsString);
+ },
+ );
+});
diff --git a/spec/frontend/lib/utils/poll_spec.js b/spec/frontend/lib/utils/poll_spec.js
index f2ca5df3672..861808e3ad8 100644
--- a/spec/frontend/lib/utils/poll_spec.js
+++ b/spec/frontend/lib/utils/poll_spec.js
@@ -1,6 +1,6 @@
import waitForPromises from 'helpers/wait_for_promises';
-import Poll from '~/lib/utils/poll';
import { successCodes } from '~/lib/utils/http_status';
+import Poll from '~/lib/utils/poll';
describe('Poll', () => {
let callbacks;
diff --git a/spec/frontend/lib/utils/poll_until_complete_spec.js b/spec/frontend/lib/utils/poll_until_complete_spec.js
index 38203c460e3..7509f954a84 100644
--- a/spec/frontend/lib/utils/poll_until_complete_spec.js
+++ b/spec/frontend/lib/utils/poll_until_complete_spec.js
@@ -1,8 +1,8 @@
import AxiosMockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
-import pollUntilComplete from '~/lib/utils/poll_until_complete';
import httpStatusCodes from '~/lib/utils/http_status';
+import pollUntilComplete from '~/lib/utils/poll_until_complete';
const endpoint = `${TEST_HOST}/foo`;
const mockData = 'mockData';
diff --git a/spec/frontend/lib/utils/unit_format/formatter_factory_spec.js b/spec/frontend/lib/utils/unit_format/formatter_factory_spec.js
index 26b942c3567..0ca70e0a77e 100644
--- a/spec/frontend/lib/utils/unit_format/formatter_factory_spec.js
+++ b/spec/frontend/lib/utils/unit_format/formatter_factory_spec.js
@@ -36,6 +36,27 @@ describe('unit_format/formatter_factory', () => {
expect(formatNumber(10 ** 7, undefined, 9)).toBe('1.00e+7');
expect(formatNumber(10 ** 7, undefined, 10)).toBe('10,000,000');
});
+
+ describe('formats with a different locale', () => {
+ let originalLang;
+
+ beforeAll(() => {
+ originalLang = document.documentElement.lang;
+ document.documentElement.lang = 'es';
+ });
+
+ afterAll(() => {
+ document.documentElement.lang = originalLang;
+ });
+
+ it('formats a using the correct thousands separator', () => {
+ expect(formatNumber(1000000)).toBe('1.000.000');
+ });
+
+ it('formats a using the correct decimal separator', () => {
+ expect(formatNumber(12.345)).toBe('12,345');
+ });
+ });
});
describe('suffixFormatter', () => {
diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js
index 5846acbdb79..b60ddea81ee 100644
--- a/spec/frontend/lib/utils/url_utility_spec.js
+++ b/spec/frontend/lib/utils/url_utility_spec.js
@@ -492,6 +492,28 @@ describe('URL utility', () => {
});
});
+ describe('isExternal', () => {
+ const gitlabUrl = 'https://gitlab.com/';
+
+ beforeEach(() => {
+ gon.gitlab_url = gitlabUrl;
+ });
+
+ afterEach(() => {
+ gon.gitlab_url = '';
+ });
+
+ it.each`
+ url | urlType | external
+ ${'/gitlab-org/gitlab-test/-/issues/2'} | ${'relative'} | ${false}
+ ${gitlabUrl} | ${'absolute and internal'} | ${false}
+ ${`${gitlabUrl}/gitlab-org/gitlab-test`} | ${'absolute and internal'} | ${false}
+ ${'http://jira.atlassian.net/browse/IG-1'} | ${'absolute and external'} | ${true}
+ `('returns $external for $url ($urlType)', ({ url, external }) => {
+ expect(urlUtils.isExternal(url)).toBe(external);
+ });
+ });
+
describe('isBase64DataUrl', () => {
it.each`
url | valid
@@ -858,4 +880,37 @@ describe('URL utility', () => {
expect(urlUtils.getURLOrigin(url)).toBe(expectation);
});
});
+
+ describe('encodeSaferUrl', () => {
+ it.each`
+ character | input | output
+ ${' '} | ${'/url/hello 1.jpg'} | ${'/url/hello%201.jpg'}
+ ${'#'} | ${'/url/hello#1.jpg'} | ${'/url/hello%231.jpg'}
+ ${'!'} | ${'/url/hello!.jpg'} | ${'/url/hello%21.jpg'}
+ ${'~'} | ${'/url/hello~.jpg'} | ${'/url/hello%7E.jpg'}
+ ${'*'} | ${'/url/hello*.jpg'} | ${'/url/hello%2A.jpg'}
+ ${"'"} | ${"/url/hello'.jpg"} | ${'/url/hello%27.jpg'}
+ ${'('} | ${'/url/hello(.jpg'} | ${'/url/hello%28.jpg'}
+ ${')'} | ${'/url/hello).jpg'} | ${'/url/hello%29.jpg'}
+ ${'?'} | ${'/url/hello?.jpg'} | ${'/url/hello%3F.jpg'}
+ ${'='} | ${'/url/hello=.jpg'} | ${'/url/hello%3D.jpg'}
+ ${'+'} | ${'/url/hello+.jpg'} | ${'/url/hello%2B.jpg'}
+ ${'&'} | ${'/url/hello&.jpg'} | ${'/url/hello%26.jpg'}
+ `(
+ 'properly escapes `$character` characters while retaining the integrity of the URL',
+ ({ input, output }) => {
+ expect(urlUtils.encodeSaferUrl(input)).toBe(output);
+ },
+ );
+
+ it.each`
+ character | input
+ ${'/, .'} | ${'/url/hello.png'}
+ ${'\\d'} | ${'/url/hello123.png'}
+ ${'-'} | ${'/url/hello-123.png'}
+ ${'_'} | ${'/url/hello_123.png'}
+ `('makes no changes to unproblematic characters ($character)', ({ input }) => {
+ expect(urlUtils.encodeSaferUrl(input)).toBe(input);
+ });
+ });
});
diff --git a/spec/frontend/line_highlighter_spec.js b/spec/frontend/line_highlighter_spec.js
index 1091bbf8aba..8318f63ab3e 100644
--- a/spec/frontend/line_highlighter_spec.js
+++ b/spec/frontend/line_highlighter_spec.js
@@ -1,8 +1,8 @@
/* eslint-disable no-return-assign, no-new, no-underscore-dangle */
import $ from 'jquery';
-import LineHighlighter from '~/line_highlighter';
import * as utils from '~/lib/utils/common_utils';
+import LineHighlighter from '~/line_highlighter';
describe('LineHighlighter', () => {
const testContext = {};
diff --git a/spec/frontend/logs/components/environment_logs_spec.js b/spec/frontend/logs/components/environment_logs_spec.js
index 351ff317feb..b40d9d7d5e2 100644
--- a/spec/frontend/logs/components/environment_logs_spec.js
+++ b/spec/frontend/logs/components/environment_logs_spec.js
@@ -1,9 +1,9 @@
import { GlSprintf, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { scrollDown } from '~/lib/utils/scroll_utils';
import EnvironmentLogs from '~/logs/components/environment_logs.vue';
import { createStore } from '~/logs/stores';
-import { scrollDown } from '~/lib/utils/scroll_utils';
import {
mockEnvName,
mockEnvironments,
diff --git a/spec/frontend/logs/components/log_advanced_filters_spec.js b/spec/frontend/logs/components/log_advanced_filters_spec.js
index dfa8913a301..111542ff33e 100644
--- a/spec/frontend/logs/components/log_advanced_filters_spec.js
+++ b/spec/frontend/logs/components/log_advanced_filters_spec.js
@@ -1,13 +1,12 @@
-import { shallowMount } from '@vue/test-utils';
import { GlFilteredSearch } from '@gitlab/ui';
-import { defaultTimeRange } from '~/vue_shared/constants';
+import { shallowMount } from '@vue/test-utils';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
-import { createStore } from '~/logs/stores';
+import LogAdvancedFilters from '~/logs/components/log_advanced_filters.vue';
import { TOKEN_TYPE_POD_NAME } from '~/logs/constants';
+import { createStore } from '~/logs/stores';
+import { defaultTimeRange } from '~/vue_shared/constants';
import { mockPods, mockSearch } from '../mock_data';
-import LogAdvancedFilters from '~/logs/components/log_advanced_filters.vue';
-
const module = 'environmentLogs';
describe('LogAdvancedFilters', () => {
diff --git a/spec/frontend/logs/components/log_control_buttons_spec.js b/spec/frontend/logs/components/log_control_buttons_spec.js
index 55b28445786..9c1617e4daa 100644
--- a/spec/frontend/logs/components/log_control_buttons_spec.js
+++ b/spec/frontend/logs/components/log_control_buttons_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import LogControlButtons from '~/logs/components/log_control_buttons.vue';
describe('LogControlButtons', () => {
diff --git a/spec/frontend/logs/components/log_simple_filters_spec.js b/spec/frontend/logs/components/log_simple_filters_spec.js
index 5bd42fd7dbc..04ad2e03542 100644
--- a/spec/frontend/logs/components/log_simple_filters_spec.js
+++ b/spec/frontend/logs/components/log_simple_filters_spec.js
@@ -1,10 +1,9 @@
import { GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import LogSimpleFilters from '~/logs/components/log_simple_filters.vue';
import { createStore } from '~/logs/stores';
import { mockPods, mockPodName } from '../mock_data';
-import LogSimpleFilters from '~/logs/components/log_simple_filters.vue';
-
const module = 'environmentLogs';
describe('LogSimpleFilters', () => {
diff --git a/spec/frontend/logs/stores/actions_spec.js b/spec/frontend/logs/stores/actions_spec.js
index bc58f1e677f..92c2f82af27 100644
--- a/spec/frontend/logs/stores/actions_spec.js
+++ b/spec/frontend/logs/stores/actions_spec.js
@@ -1,9 +1,9 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import Tracking from '~/tracking';
-import * as types from '~/logs/stores/mutation_types';
+import { deprecatedCreateFlash as flash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
-import logsPageState from '~/logs/stores/state';
+import { TOKEN_TYPE_POD_NAME } from '~/logs/constants';
import {
setInitData,
showFilteredLogs,
@@ -13,12 +13,12 @@ import {
fetchMoreLogsPrepend,
fetchManagedApps,
} from '~/logs/stores/actions';
+import * as types from '~/logs/stores/mutation_types';
+import logsPageState from '~/logs/stores/state';
+import Tracking from '~/tracking';
import { defaultTimeRange } from '~/vue_shared/constants';
-import axios from '~/lib/utils/axios_utils';
-import { deprecatedCreateFlash as flash } from '~/flash';
-
import {
mockPodName,
mockEnvironmentsEndpoint,
@@ -34,7 +34,6 @@ import {
mockManagedApps,
mockManagedAppsEndpoint,
} from '../mock_data';
-import { TOKEN_TYPE_POD_NAME } from '~/logs/constants';
jest.mock('~/flash');
jest.mock('~/lib/utils/datetime_range');
diff --git a/spec/frontend/logs/stores/mutations_spec.js b/spec/frontend/logs/stores/mutations_spec.js
index 51f6494b011..111c795ba52 100644
--- a/spec/frontend/logs/stores/mutations_spec.js
+++ b/spec/frontend/logs/stores/mutations_spec.js
@@ -1,5 +1,5 @@
-import mutations from '~/logs/stores/mutations';
import * as types from '~/logs/stores/mutation_types';
+import mutations from '~/logs/stores/mutations';
import logsPageState from '~/logs/stores/state';
import {
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 30166e2d5ae..f86237dc160 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
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import AccessRequestActionButtons from '~/members/components/action_buttons/access_request_action_buttons.vue';
-import RemoveMemberButton from '~/members/components/action_buttons/remove_member_button.vue';
import ApproveAccessRequestButton from '~/members/components/action_buttons/approve_access_request_button.vue';
+import RemoveMemberButton from '~/members/components/action_buttons/remove_member_button.vue';
import { accessRequest as member } from '../../mock_data';
describe('AccessRequestActionButtons', () => {
diff --git a/spec/frontend/members/components/action_buttons/approve_access_request_button_spec.js b/spec/frontend/members/components/action_buttons/approve_access_request_button_spec.js
index 7ce2c633bb3..f77d41a642e 100644
--- a/spec/frontend/members/components/action_buttons/approve_access_request_button_spec.js
+++ b/spec/frontend/members/components/action_buttons/approve_access_request_button_spec.js
@@ -1,6 +1,6 @@
+import { GlButton, GlForm } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import { GlButton, GlForm } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import ApproveAccessRequestButton from '~/members/components/action_buttons/approve_access_request_button.vue';
diff --git a/spec/frontend/members/components/action_buttons/leave_button_spec.js b/spec/frontend/members/components/action_buttons/leave_button_spec.js
index 2afe112c74b..4859d033464 100644
--- a/spec/frontend/members/components/action_buttons/leave_button_spec.js
+++ b/spec/frontend/members/components/action_buttons/leave_button_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import LeaveButton from '~/members/components/action_buttons/leave_button.vue';
import LeaveModal from '~/members/components/modals/leave_modal.vue';
diff --git a/spec/frontend/members/components/action_buttons/remove_group_link_button_spec.js b/spec/frontend/members/components/action_buttons/remove_group_link_button_spec.js
index 45283788676..f6e342898cb 100644
--- a/spec/frontend/members/components/action_buttons/remove_group_link_button_spec.js
+++ b/spec/frontend/members/components/action_buttons/remove_group_link_button_spec.js
@@ -1,6 +1,6 @@
+import { GlButton } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import { GlButton } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import RemoveGroupLinkButton from '~/members/components/action_buttons/remove_group_link_button.vue';
import { group } from '../../mock_data';
diff --git a/spec/frontend/members/components/action_buttons/resend_invite_button_spec.js b/spec/frontend/members/components/action_buttons/resend_invite_button_spec.js
index 05ea0dc2886..49b6979f954 100644
--- a/spec/frontend/members/components/action_buttons/resend_invite_button_spec.js
+++ b/spec/frontend/members/components/action_buttons/resend_invite_button_spec.js
@@ -1,6 +1,6 @@
+import { GlButton } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import { GlButton } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import ResendInviteButton from '~/members/components/action_buttons/resend_invite_button.vue';
diff --git a/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js b/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js
index f28e5040006..1d7ea5b3109 100644
--- a/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js
+++ b/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
-import UserActionButtons from '~/members/components/action_buttons/user_action_buttons.vue';
-import RemoveMemberButton from '~/members/components/action_buttons/remove_member_button.vue';
import LeaveButton from '~/members/components/action_buttons/leave_button.vue';
+import RemoveMemberButton from '~/members/components/action_buttons/remove_member_button.vue';
+import UserActionButtons from '~/members/components/action_buttons/user_action_buttons.vue';
import { member, orphanedMember } from '../../mock_data';
describe('UserActionButtons', () => {
diff --git a/spec/frontend/groups/members/components/app_spec.js b/spec/frontend/members/components/app_spec.js
index 9847dacbec8..a1329c3ee9f 100644
--- a/spec/frontend/groups/members/components/app_spec.js
+++ b/spec/frontend/members/components/app_spec.js
@@ -1,14 +1,14 @@
+import { GlAlert } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue';
import Vuex from 'vuex';
-import { GlAlert } from '@gitlab/ui';
-import App from '~/groups/members/components/app.vue';
-import FilterSortContainer from '~/members/components/filter_sort/filter_sort_container.vue';
import * as commonUtils from '~/lib/utils/common_utils';
+import MembersApp from '~/members/components/app.vue';
+import FilterSortContainer from '~/members/components/filter_sort/filter_sort_container.vue';
import { RECEIVE_MEMBER_ROLE_ERROR, HIDE_ERROR } from '~/members/store/mutation_types';
import mutations from '~/members/store/mutations';
-describe('GroupMembersApp', () => {
+describe('MembersApp', () => {
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -25,7 +25,7 @@ describe('GroupMembersApp', () => {
mutations,
});
- wrapper = shallowMount(App, {
+ wrapper = shallowMount(MembersApp, {
localVue,
store,
...options,
@@ -48,7 +48,7 @@ describe('GroupMembersApp', () => {
it('renders and scrolls to error alert', async () => {
createComponent({ showError: false, errorMessage: '' });
- store.commit(RECEIVE_MEMBER_ROLE_ERROR);
+ store.commit(RECEIVE_MEMBER_ROLE_ERROR, { error: new Error('Network Error') });
await nextTick();
diff --git a/spec/frontend/members/components/avatars/group_avatar_spec.js b/spec/frontend/members/components/avatars/group_avatar_spec.js
index 658bb9462b0..9c1574a84ee 100644
--- a/spec/frontend/members/components/avatars/group_avatar_spec.js
+++ b/spec/frontend/members/components/avatars/group_avatar_spec.js
@@ -1,8 +1,8 @@
-import { mount, createWrapper } from '@vue/test-utils';
-import { getByText as getByTextHelper } from '@testing-library/dom';
import { GlAvatarLink } from '@gitlab/ui';
-import { group as member } from '../../mock_data';
+import { getByText as getByTextHelper } from '@testing-library/dom';
+import { mount, createWrapper } from '@vue/test-utils';
import GroupAvatar from '~/members/components/avatars/group_avatar.vue';
+import { group as member } from '../../mock_data';
describe('MemberList', () => {
let wrapper;
diff --git a/spec/frontend/members/components/avatars/invite_avatar_spec.js b/spec/frontend/members/components/avatars/invite_avatar_spec.js
index 13ee727528b..b197a46c0d1 100644
--- a/spec/frontend/members/components/avatars/invite_avatar_spec.js
+++ b/spec/frontend/members/components/avatars/invite_avatar_spec.js
@@ -1,7 +1,7 @@
-import { mount, createWrapper } from '@vue/test-utils';
import { getByText as getByTextHelper } from '@testing-library/dom';
-import { invite as member } from '../../mock_data';
+import { mount, createWrapper } from '@vue/test-utils';
import InviteAvatar from '~/members/components/avatars/invite_avatar.vue';
+import { invite as member } from '../../mock_data';
describe('MemberList', () => {
let wrapper;
diff --git a/spec/frontend/members/components/avatars/user_avatar_spec.js b/spec/frontend/members/components/avatars/user_avatar_spec.js
index 411ec1a54de..303c82582a3 100644
--- a/spec/frontend/members/components/avatars/user_avatar_spec.js
+++ b/spec/frontend/members/components/avatars/user_avatar_spec.js
@@ -1,8 +1,8 @@
-import { mount, createWrapper } from '@vue/test-utils';
-import { within } from '@testing-library/dom';
import { GlAvatarLink, GlBadge } from '@gitlab/ui';
-import { member as memberMock, orphanedMember } from '../../mock_data';
+import { within } from '@testing-library/dom';
+import { mount, createWrapper } from '@vue/test-utils';
import UserAvatar from '~/members/components/avatars/user_avatar.vue';
+import { member as memberMock, orphanedMember } from '../../mock_data';
describe('UserAvatar', () => {
let wrapper;
diff --git a/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js b/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js
index 2bed1e803ca..14b437a8c4e 100644
--- a/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js
+++ b/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js
@@ -1,6 +1,6 @@
+import { GlFilteredSearchToken } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import { GlFilteredSearchToken } from '@gitlab/ui';
import MembersFilteredSearchBar from '~/members/components/filter_sort/members_filtered_search_bar.vue';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
diff --git a/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js b/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js
index d98c9116512..357fad741e9 100644
--- a/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js
+++ b/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js
@@ -1,8 +1,8 @@
+import { GlSorting, GlSortingItem } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import { GlSorting, GlSortingItem } from '@gitlab/ui';
-import SortDropdown from '~/members/components/filter_sort/sort_dropdown.vue';
import * as urlUtilities from '~/lib/utils/url_utility';
+import SortDropdown from '~/members/components/filter_sort/sort_dropdown.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
diff --git a/spec/frontend/members/components/modals/leave_modal_spec.js b/spec/frontend/members/components/modals/leave_modal_spec.js
index dca47d1f6af..2d52911572f 100644
--- a/spec/frontend/members/components/modals/leave_modal_spec.js
+++ b/spec/frontend/members/components/modals/leave_modal_spec.js
@@ -1,7 +1,7 @@
-import { mount, createLocalVue, createWrapper } from '@vue/test-utils';
import { GlModal, GlForm } from '@gitlab/ui';
-import { nextTick } from 'vue';
import { within } from '@testing-library/dom';
+import { mount, createLocalVue, createWrapper } from '@vue/test-utils';
+import { nextTick } from 'vue';
import Vuex from 'vuex';
import LeaveModal from '~/members/components/modals/leave_modal.vue';
import { LEAVE_MODAL_ID } from '~/members/constants';
diff --git a/spec/frontend/members/components/modals/remove_group_link_modal_spec.js b/spec/frontend/members/components/modals/remove_group_link_modal_spec.js
index 234857419b6..62df912c1a2 100644
--- a/spec/frontend/members/components/modals/remove_group_link_modal_spec.js
+++ b/spec/frontend/members/components/modals/remove_group_link_modal_spec.js
@@ -1,7 +1,7 @@
-import { mount, createLocalVue, createWrapper } from '@vue/test-utils';
import { GlModal, GlForm } from '@gitlab/ui';
-import { nextTick } from 'vue';
import { within } from '@testing-library/dom';
+import { mount, createLocalVue, createWrapper } from '@vue/test-utils';
+import { nextTick } from 'vue';
import Vuex from 'vuex';
import RemoveGroupLinkModal from '~/members/components/modals/remove_group_link_modal.vue';
import { REMOVE_GROUP_LINK_MODAL_ID } from '~/members/constants';
diff --git a/spec/frontend/members/components/table/created_at_spec.js b/spec/frontend/members/components/table/created_at_spec.js
index dc1f62722ab..74b71e22893 100644
--- a/spec/frontend/members/components/table/created_at_spec.js
+++ b/spec/frontend/members/components/table/created_at_spec.js
@@ -1,5 +1,5 @@
-import { mount, createWrapper } from '@vue/test-utils';
import { within } from '@testing-library/dom';
+import { mount, createWrapper } from '@vue/test-utils';
import { useFakeDate } from 'helpers/fake_date';
import CreatedAt from '~/members/components/table/created_at.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
diff --git a/spec/frontend/members/components/table/expiration_datepicker_spec.js b/spec/frontend/members/components/table/expiration_datepicker_spec.js
index 0caaafb8d7b..d26172b4ed1 100644
--- a/spec/frontend/members/components/table/expiration_datepicker_spec.js
+++ b/spec/frontend/members/components/table/expiration_datepicker_spec.js
@@ -1,7 +1,7 @@
+import { GlDatepicker } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
-import Vuex from 'vuex';
import { nextTick } from 'vue';
-import { GlDatepicker } from '@gitlab/ui';
+import Vuex from 'vuex';
import { useFakeDate } from 'helpers/fake_date';
import waitForPromises from 'helpers/wait_for_promises';
import ExpirationDatepicker from '~/members/components/table/expiration_datepicker.vue';
diff --git a/spec/frontend/members/components/table/expires_at_spec.js b/spec/frontend/members/components/table/expires_at_spec.js
index 321008727cd..02fe3c6d684 100644
--- a/spec/frontend/members/components/table/expires_at_spec.js
+++ b/spec/frontend/members/components/table/expires_at_spec.js
@@ -1,5 +1,5 @@
-import { mount, createWrapper } from '@vue/test-utils';
import { within } from '@testing-library/dom';
+import { mount, createWrapper } from '@vue/test-utils';
import { useFakeDate } from 'helpers/fake_date';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import ExpiresAt from '~/members/components/table/expires_at.vue';
diff --git a/spec/frontend/members/components/table/member_action_buttons_spec.js b/spec/frontend/members/components/table/member_action_buttons_spec.js
index b7a6df3d054..546d09732d6 100644
--- a/spec/frontend/members/components/table/member_action_buttons_spec.js
+++ b/spec/frontend/members/components/table/member_action_buttons_spec.js
@@ -1,11 +1,11 @@
import { shallowMount } from '@vue/test-utils';
-import { MEMBER_TYPES } from '~/members/constants';
-import { member as memberMock, group, invite, accessRequest } from '../../mock_data';
-import MemberActionButtons from '~/members/components/table/member_action_buttons.vue';
-import UserActionButtons from '~/members/components/action_buttons/user_action_buttons.vue';
+import AccessRequestActionButtons from '~/members/components/action_buttons/access_request_action_buttons.vue';
import GroupActionButtons from '~/members/components/action_buttons/group_action_buttons.vue';
import InviteActionButtons from '~/members/components/action_buttons/invite_action_buttons.vue';
-import AccessRequestActionButtons from '~/members/components/action_buttons/access_request_action_buttons.vue';
+import UserActionButtons from '~/members/components/action_buttons/user_action_buttons.vue';
+import MemberActionButtons from '~/members/components/table/member_action_buttons.vue';
+import { MEMBER_TYPES } from '~/members/constants';
+import { member as memberMock, group, invite, accessRequest } from '../../mock_data';
describe('MemberActionButtons', () => {
let wrapper;
diff --git a/spec/frontend/members/components/table/member_avatar_spec.js b/spec/frontend/members/components/table/member_avatar_spec.js
index 4341dfbbaf9..3cce64effbc 100644
--- a/spec/frontend/members/components/table/member_avatar_spec.js
+++ b/spec/frontend/members/components/table/member_avatar_spec.js
@@ -1,10 +1,10 @@
import { shallowMount } from '@vue/test-utils';
-import { MEMBER_TYPES } from '~/members/constants';
-import { member as memberMock, group, invite, accessRequest } from '../../mock_data';
-import MemberAvatar from '~/members/components/table/member_avatar.vue';
-import UserAvatar from '~/members/components/avatars/user_avatar.vue';
import GroupAvatar from '~/members/components/avatars/group_avatar.vue';
import InviteAvatar from '~/members/components/avatars/invite_avatar.vue';
+import UserAvatar from '~/members/components/avatars/user_avatar.vue';
+import MemberAvatar from '~/members/components/table/member_avatar.vue';
+import { MEMBER_TYPES } from '~/members/constants';
+import { member as memberMock, group, invite, accessRequest } from '../../mock_data';
describe('MemberList', () => {
let wrapper;
diff --git a/spec/frontend/members/components/table/member_source_spec.js b/spec/frontend/members/components/table/member_source_spec.js
index 95547090aed..2cd888207b1 100644
--- a/spec/frontend/members/components/table/member_source_spec.js
+++ b/spec/frontend/members/components/table/member_source_spec.js
@@ -1,5 +1,5 @@
-import { mount, createWrapper } from '@vue/test-utils';
import { getByText as getByTextHelper } from '@testing-library/dom';
+import { mount, createWrapper } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import MemberSource from '~/members/components/table/member_source.vue';
diff --git a/spec/frontend/members/components/table/members_table_cell_spec.js b/spec/frontend/members/components/table/members_table_cell_spec.js
index 117c9255c00..b7dcd2a9fae 100644
--- a/spec/frontend/members/components/table/members_table_cell_spec.js
+++ b/spec/frontend/members/components/table/members_table_cell_spec.js
@@ -1,8 +1,15 @@
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import { MEMBER_TYPES } from '~/members/constants';
-import { member as memberMock, group, invite, accessRequest } from '../../mock_data';
import MembersTableCell from '~/members/components/table/members_table_cell.vue';
+import { MEMBER_TYPES } from '~/members/constants';
+import {
+ member as memberMock,
+ directMember,
+ inheritedMember,
+ group,
+ invite,
+ accessRequest,
+} from '../../mock_data';
describe('MembersTableCell', () => {
const WrappedComponent = {
@@ -31,7 +38,7 @@ describe('MembersTableCell', () => {
const localVue = createLocalVue();
localVue.use(Vuex);
- localVue.component('wrapped-component', WrappedComponent);
+ localVue.component('WrappedComponent', WrappedComponent);
const createStore = (state = {}) => {
return new Vuex.Store({
@@ -75,19 +82,12 @@ describe('MembersTableCell', () => {
const createComponentWithDirectMember = (member = {}) => {
createComponent({
- member: {
- ...memberMock,
- source: {
- ...memberMock.source,
- id: 1,
- },
- ...member,
- },
+ member: { ...directMember, ...member },
});
};
const createComponentWithInheritedMember = (member = {}) => {
createComponent({
- member: { ...memberMock, ...member },
+ member: { ...inheritedMember, ...member },
});
};
diff --git a/spec/frontend/members/components/table/members_table_spec.js b/spec/frontend/members/components/table/members_table_spec.js
index dbaccde069c..cf5811e72e7 100644
--- a/spec/frontend/members/components/table/members_table_spec.js
+++ b/spec/frontend/members/components/table/members_table_spec.js
@@ -1,21 +1,21 @@
-import { mount, createLocalVue, createWrapper } from '@vue/test-utils';
-import Vuex from 'vuex';
+import { GlBadge, GlTable } from '@gitlab/ui';
import {
getByText as getByTextHelper,
getByTestId as getByTestIdHelper,
within,
} from '@testing-library/dom';
-import { GlBadge, GlTable } from '@gitlab/ui';
-import MembersTable from '~/members/components/table/members_table.vue';
-import MemberAvatar from '~/members/components/table/member_avatar.vue';
-import MemberSource from '~/members/components/table/member_source.vue';
-import ExpiresAt from '~/members/components/table/expires_at.vue';
+import { mount, createLocalVue, createWrapper } from '@vue/test-utils';
+import Vuex from 'vuex';
import CreatedAt from '~/members/components/table/created_at.vue';
-import RoleDropdown from '~/members/components/table/role_dropdown.vue';
import ExpirationDatepicker from '~/members/components/table/expiration_datepicker.vue';
+import ExpiresAt from '~/members/components/table/expires_at.vue';
import MemberActionButtons from '~/members/components/table/member_action_buttons.vue';
+import MemberAvatar from '~/members/components/table/member_avatar.vue';
+import MemberSource from '~/members/components/table/member_source.vue';
+import MembersTable from '~/members/components/table/members_table.vue';
+import RoleDropdown from '~/members/components/table/role_dropdown.vue';
import * as initUserPopovers from '~/user_popovers';
-import { member as memberMock, invite, accessRequest } from '../../mock_data';
+import { member as memberMock, directMember, invite, accessRequest } from '../../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -74,11 +74,6 @@ describe('MembersTable', () => {
});
describe('fields', () => {
- const directMember = {
- ...memberMock,
- source: { ...memberMock.source, id: 1 },
- };
-
const memberCanUpdate = {
...directMember,
canUpdate: true,
@@ -154,7 +149,7 @@ describe('MembersTable', () => {
expect(findTableCellByMemberId('Actions', members[0].id).classes()).toStrictEqual([
'col-actions',
'gl-display-none!',
- 'gl-display-lg-table-cell!',
+ 'gl-lg-display-table-cell!',
]);
expect(findTableCellByMemberId('Actions', members[1].id).classes()).toStrictEqual([
'col-actions',
diff --git a/spec/frontend/members/components/table/role_dropdown_spec.js b/spec/frontend/members/components/table/role_dropdown_spec.js
index 96a388614f3..aa280599061 100644
--- a/spec/frontend/members/components/table/role_dropdown_spec.js
+++ b/spec/frontend/members/components/table/role_dropdown_spec.js
@@ -1,10 +1,11 @@
-import { mount, createWrapper, createLocalVue } from '@vue/test-utils';
-import Vuex from 'vuex';
-import { nextTick } from 'vue';
-import { within } from '@testing-library/dom';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
+import { within } from '@testing-library/dom';
+import { mount, createWrapper, createLocalVue } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import Vuex from 'vuex';
import waitForPromises from 'helpers/wait_for_promises';
+import { BV_DROPDOWN_SHOW } from '~/lib/utils/constants';
import RoleDropdown from '~/members/components/table/role_dropdown.vue';
import { member } from '../../mock_data';
@@ -67,7 +68,7 @@ describe('RoleDropdown', () => {
createComponent();
findDropdownToggle().trigger('click');
- wrapper.vm.$root.$on('bv::dropdown::shown', () => {
+ wrapper.vm.$root.$on(BV_DROPDOWN_SHOW, () => {
done();
});
});
diff --git a/spec/frontend/groups/members/index_spec.js b/spec/frontend/members/index_spec.js
index 5c717e53229..dd3b9ddd912 100644
--- a/spec/frontend/groups/members/index_spec.js
+++ b/spec/frontend/members/index_spec.js
@@ -1,15 +1,15 @@
import { createWrapper } from '@vue/test-utils';
-import { initGroupMembersApp } from '~/groups/members';
-import GroupMembersApp from '~/groups/members/components/app.vue';
-import { membersJsonString, membersParsed } from './mock_data';
+import MembersApp from '~/members/components/app.vue';
+import { initMembersApp } from '~/members/index';
+import { membersJsonString, members } from './mock_data';
-describe('initGroupMembersApp', () => {
+describe('initMembersApp', () => {
let el;
let vm;
let wrapper;
const setup = () => {
- vm = initGroupMembersApp(el, {
+ vm = initMembersApp(el, {
tableFields: ['account'],
tableAttrs: { table: { 'data-qa-selector': 'members_list' } },
tableSortableFields: ['account'],
@@ -22,7 +22,7 @@ describe('initGroupMembersApp', () => {
beforeEach(() => {
el = document.createElement('div');
el.setAttribute('data-members', membersJsonString);
- el.setAttribute('data-group-id', '234');
+ el.setAttribute('data-source-id', '234');
el.setAttribute('data-can-manage-members', 'true');
el.setAttribute('data-member-path', '/groups/foo-bar/-/group_members/:id');
@@ -36,10 +36,10 @@ describe('initGroupMembersApp', () => {
wrapper = null;
});
- it('renders `GroupMembersApp`', () => {
+ it('renders `MembersApp`', () => {
setup();
- expect(wrapper.find(GroupMembersApp).exists()).toBe(true);
+ expect(wrapper.find(MembersApp).exists()).toBe(true);
});
it('sets `currentUserId` in Vuex store', () => {
@@ -57,7 +57,7 @@ describe('initGroupMembersApp', () => {
});
});
- it('parses and sets `data-group-id` as `sourceId` in Vuex store', () => {
+ it('parses and sets `data-source-id` as `sourceId` in Vuex store', () => {
setup();
expect(vm.$store.state.sourceId).toBe(234);
@@ -72,7 +72,7 @@ describe('initGroupMembersApp', () => {
it('parses and sets `members` in Vuex store', () => {
setup();
- expect(vm.$store.state.members).toEqual(membersParsed);
+ expect(vm.$store.state.members).toEqual(members);
});
it('sets `tableFields` in Vuex store', () => {
diff --git a/spec/frontend/members/mock_data.js b/spec/frontend/members/mock_data.js
index e668f2a1998..fa324ce1cf9 100644
--- a/spec/frontend/members/mock_data.js
+++ b/spec/frontend/members/mock_data.js
@@ -4,6 +4,7 @@ export const member = {
canRemove: false,
canOverride: false,
isOverridden: false,
+ isDirectMember: false,
accessLevel: { integerValue: 50, stringValue: 'Owner' },
source: {
id: 178,
@@ -69,3 +70,8 @@ export const accessRequest = {
};
export const members = [member];
+
+export const membersJsonString = JSON.stringify(members);
+
+export const directMember = { ...member, isDirectMember: true };
+export const inheritedMember = { ...member, isDirectMember: false };
diff --git a/spec/frontend/members/store/actions_spec.js b/spec/frontend/members/store/actions_spec.js
index 5424fee0750..d913c5c56df 100644
--- a/spec/frontend/members/store/actions_spec.js
+++ b/spec/frontend/members/store/actions_spec.js
@@ -1,17 +1,17 @@
-import { noop } from 'lodash';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
-import { members, group } from 'jest/members/mock_data';
-import testAction from 'helpers/vuex_action_helper';
+import { noop } from 'lodash';
import { useFakeDate } from 'helpers/fake_date';
+import testAction from 'helpers/vuex_action_helper';
+import { members, group } from 'jest/members/mock_data';
import httpStatusCodes from '~/lib/utils/http_status';
-import * as types from '~/members/store/mutation_types';
import {
updateMemberRole,
showRemoveGroupLinkModal,
hideRemoveGroupLinkModal,
updateMemberExpiration,
} from '~/members/store/actions';
+import * as types from '~/members/store/mutation_types';
describe('Vuex members actions', () => {
describe('update member actions', () => {
@@ -57,15 +57,17 @@ describe('Vuex members actions', () => {
describe('unsuccessful request', () => {
it(`commits ${types.RECEIVE_MEMBER_ROLE_ERROR} mutation and throws error`, async () => {
- mock.onPut().networkError();
+ const error = new Error('Network Error');
+ mock.onPut().reply(() => Promise.reject(error));
await expect(
testAction(updateMemberRole, payload, state, [
{
type: types.RECEIVE_MEMBER_ROLE_ERROR,
+ payload: { error },
},
]),
- ).rejects.toThrowError(new Error('Network Error'));
+ ).rejects.toThrowError(error);
});
});
});
@@ -108,15 +110,17 @@ describe('Vuex members actions', () => {
describe('unsuccessful request', () => {
it(`commits ${types.RECEIVE_MEMBER_EXPIRATION_ERROR} mutation and throws error`, async () => {
- mock.onPut().networkError();
+ const error = new Error('Network Error');
+ mock.onPut().reply(() => Promise.reject(error));
await expect(
testAction(updateMemberExpiration, { memberId, expiresAt }, state, [
{
type: types.RECEIVE_MEMBER_EXPIRATION_ERROR,
+ payload: { error },
},
]),
- ).rejects.toThrowError(new Error('Network Error'));
+ ).rejects.toThrowError(error);
});
});
});
diff --git a/spec/frontend/members/store/mutations_spec.js b/spec/frontend/members/store/mutations_spec.js
index 488bfdf15fd..7ad7034eb6d 100644
--- a/spec/frontend/members/store/mutations_spec.js
+++ b/spec/frontend/members/store/mutations_spec.js
@@ -1,6 +1,6 @@
import { members, group } from 'jest/members/mock_data';
-import mutations from '~/members/store/mutations';
import * as types from '~/members/store/mutation_types';
+import mutations from '~/members/store/mutations';
describe('Vuex members mutations', () => {
describe('update member mutations', () => {
@@ -28,13 +28,33 @@ describe('Vuex members mutations', () => {
});
describe(types.RECEIVE_MEMBER_ROLE_ERROR, () => {
- it('shows error message', () => {
- mutations[types.RECEIVE_MEMBER_ROLE_ERROR](state);
+ describe('when error does not have a message', () => {
+ it('shows default error message', () => {
+ mutations[types.RECEIVE_MEMBER_ROLE_ERROR](state, {
+ error: new Error('Network Error'),
+ });
+
+ expect(state.showError).toBe(true);
+ expect(state.errorMessage).toBe(
+ "An error occurred while updating the member's role, please try again.",
+ );
+ });
+ });
+
+ describe('when error has a message', () => {
+ it('shows error message', () => {
+ const error = new Error('Request failed with status code 422');
+ const message =
+ 'User email "john.smith@gmail.com" does not match the allowed domain of example.com';
+
+ error.response = {
+ data: { message },
+ };
+ mutations[types.RECEIVE_MEMBER_ROLE_ERROR](state, { error });
- expect(state.showError).toBe(true);
- expect(state.errorMessage).toBe(
- "An error occurred while updating the member's role, please try again.",
- );
+ expect(state.showError).toBe(true);
+ expect(state.errorMessage).toBe(message);
+ });
});
});
@@ -52,13 +72,33 @@ describe('Vuex members mutations', () => {
});
describe(types.RECEIVE_MEMBER_EXPIRATION_ERROR, () => {
- it('shows error message', () => {
- mutations[types.RECEIVE_MEMBER_EXPIRATION_ERROR](state);
+ describe('when error does not have a message', () => {
+ it('shows default error message', () => {
+ mutations[types.RECEIVE_MEMBER_EXPIRATION_ERROR](state, {
+ error: new Error('Network Error'),
+ });
+
+ expect(state.showError).toBe(true);
+ expect(state.errorMessage).toBe(
+ "An error occurred while updating the member's expiration date, please try again.",
+ );
+ });
+ });
- expect(state.showError).toBe(true);
- expect(state.errorMessage).toBe(
- "An error occurred while updating the member's expiration date, please try again.",
- );
+ describe('when error has a message', () => {
+ it('shows error message', () => {
+ const error = new Error('Request failed with status code 422');
+ const message =
+ 'User email "john.smith@gmail.com" does not match the allowed domain of example.com';
+
+ error.response = {
+ data: { message },
+ };
+ mutations[types.RECEIVE_MEMBER_EXPIRATION_ERROR](state, { error });
+
+ expect(state.showError).toBe(true);
+ expect(state.errorMessage).toBe(message);
+ });
});
});
});
diff --git a/spec/frontend/members/utils_spec.js b/spec/frontend/members/utils_spec.js
index 7cd4e735b55..f447a4c4ee9 100644
--- a/spec/frontend/members/utils_spec.js
+++ b/spec/frontend/members/utils_spec.js
@@ -1,3 +1,4 @@
+import { DEFAULT_SORT } from '~/members/constants';
import {
generateBadges,
isGroup,
@@ -9,12 +10,19 @@ import {
canOverride,
parseSortParam,
buildSortHref,
+ parseDataAttributes,
+ groupLinkRequestFormatter,
} from '~/members/utils';
-import { DEFAULT_SORT } from '~/members/constants';
-import { member as memberMock, group, invite } from './mock_data';
+import {
+ member as memberMock,
+ directMember,
+ inheritedMember,
+ group,
+ invite,
+ membersJsonString,
+ members,
+} from './mock_data';
-const DIRECT_MEMBER_ID = 178;
-const INHERITED_MEMBER_ID = 179;
const IS_CURRENT_USER_ID = 123;
const IS_NOT_CURRENT_USER_ID = 124;
const URL_HOST = 'https://localhost/';
@@ -57,11 +65,11 @@ describe('Members Utils', () => {
describe('isDirectMember', () => {
test.each`
- sourceId | expected
- ${DIRECT_MEMBER_ID} | ${true}
- ${INHERITED_MEMBER_ID} | ${false}
- `('returns $expected', ({ sourceId, expected }) => {
- expect(isDirectMember(memberMock, sourceId)).toBe(expected);
+ member | expected
+ ${directMember} | ${true}
+ ${inheritedMember} | ${false}
+ `('returns $expected', ({ member, expected }) => {
+ expect(isDirectMember(member)).toBe(expected);
});
});
@@ -76,18 +84,13 @@ describe('Members Utils', () => {
});
describe('canRemove', () => {
- const memberCanRemove = {
- ...memberMock,
- canRemove: true,
- };
-
test.each`
- member | sourceId | expected
- ${memberCanRemove} | ${DIRECT_MEMBER_ID} | ${true}
- ${memberCanRemove} | ${INHERITED_MEMBER_ID} | ${false}
- ${memberMock} | ${INHERITED_MEMBER_ID} | ${false}
- `('returns $expected', ({ member, sourceId, expected }) => {
- expect(canRemove(member, sourceId)).toBe(expected);
+ member | expected
+ ${{ ...directMember, canRemove: true }} | ${true}
+ ${{ ...inheritedMember, canRemove: true }} | ${false}
+ ${{ ...memberMock, canRemove: false }} | ${false}
+ `('returns $expected', ({ member, expected }) => {
+ expect(canRemove(member)).toBe(expected);
});
});
@@ -96,25 +99,20 @@ describe('Members Utils', () => {
member | expected
${invite} | ${true}
${{ ...invite, invite: { ...invite.invite, canResend: false } }} | ${false}
- `('returns $expected', ({ member, sourceId, expected }) => {
- expect(canResend(member, sourceId)).toBe(expected);
+ `('returns $expected', ({ member, expected }) => {
+ expect(canResend(member)).toBe(expected);
});
});
describe('canUpdate', () => {
- const memberCanUpdate = {
- ...memberMock,
- canUpdate: true,
- };
-
test.each`
- member | currentUserId | sourceId | expected
- ${memberCanUpdate} | ${IS_NOT_CURRENT_USER_ID} | ${DIRECT_MEMBER_ID} | ${true}
- ${memberCanUpdate} | ${IS_CURRENT_USER_ID} | ${DIRECT_MEMBER_ID} | ${false}
- ${memberCanUpdate} | ${IS_CURRENT_USER_ID} | ${INHERITED_MEMBER_ID} | ${false}
- ${memberMock} | ${IS_NOT_CURRENT_USER_ID} | ${DIRECT_MEMBER_ID} | ${false}
- `('returns $expected', ({ member, currentUserId, sourceId, expected }) => {
- expect(canUpdate(member, currentUserId, sourceId)).toBe(expected);
+ member | currentUserId | expected
+ ${{ ...directMember, canUpdate: true }} | ${IS_NOT_CURRENT_USER_ID} | ${true}
+ ${{ ...directMember, canUpdate: true }} | ${IS_CURRENT_USER_ID} | ${false}
+ ${{ ...inheritedMember, canUpdate: true }} | ${IS_CURRENT_USER_ID} | ${false}
+ ${{ ...directMember, canUpdate: false }} | ${IS_NOT_CURRENT_USER_ID} | ${false}
+ `('returns $expected', ({ member, currentUserId, expected }) => {
+ expect(canUpdate(member, currentUserId)).toBe(expected);
});
});
@@ -229,4 +227,38 @@ describe('Members Utils', () => {
});
});
});
+
+ describe('parseDataAttributes', () => {
+ let el;
+
+ beforeEach(() => {
+ el = document.createElement('div');
+ el.setAttribute('data-members', membersJsonString);
+ el.setAttribute('data-source-id', '234');
+ el.setAttribute('data-can-manage-members', 'true');
+ });
+
+ afterEach(() => {
+ el = null;
+ });
+
+ it('correctly parses the data attributes', () => {
+ expect(parseDataAttributes(el)).toEqual({
+ members,
+ sourceId: 234,
+ canManageMembers: true,
+ });
+ });
+ });
+
+ describe('groupLinkRequestFormatter', () => {
+ it('returns expected format', () => {
+ expect(
+ groupLinkRequestFormatter({
+ accessLevel: 50,
+ expires_at: '2020-10-16',
+ }),
+ ).toEqual({ group_link: { group_access: 50, expires_at: '2020-10-16' } });
+ });
+ });
});
diff --git a/spec/frontend/merge_request/components/status_box_spec.js b/spec/frontend/merge_request/components/status_box_spec.js
index e6b6512476b..9212ae19c2d 100644
--- a/spec/frontend/merge_request/components/status_box_spec.js
+++ b/spec/frontend/merge_request/components/status_box_spec.js
@@ -1,6 +1,6 @@
-import { nextTick } from 'vue';
-import { shallowMount } from '@vue/test-utils';
import { GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import StatusBox from '~/merge_request/components/status_box.vue';
import mrEventHub from '~/merge_request/eventhub';
@@ -18,6 +18,12 @@ const testCases = [
icon: 'issue-open-m',
},
{
+ name: 'Open',
+ state: 'locked',
+ class: 'status-box-open',
+ icon: 'issue-open-m',
+ },
+ {
name: 'Closed',
state: 'closed',
class: 'status-box-mr-closed',
diff --git a/spec/frontend/merge_request_spec.js b/spec/frontend/merge_request_spec.js
index 5624043260a..84647a108b2 100644
--- a/spec/frontend/merge_request_spec.js
+++ b/spec/frontend/merge_request_spec.js
@@ -1,5 +1,5 @@
-import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter';
+import $ from 'jquery';
import { TEST_HOST } from 'spec/test_constants';
import axios from '~/lib/utils/axios_utils';
import MergeRequest from '~/merge_request';
diff --git a/spec/frontend/merge_request_tabs_spec.js b/spec/frontend/merge_request_tabs_spec.js
index 82462036219..fd2c240aff3 100644
--- a/spec/frontend/merge_request_tabs_spec.js
+++ b/spec/frontend/merge_request_tabs_spec.js
@@ -1,9 +1,8 @@
-import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter';
+import $ from 'jquery';
import initMrPage from 'helpers/init_vue_mr_page_helper';
import axios from '~/lib/utils/axios_utils';
import MergeRequestTabs from '~/merge_request_tabs';
-import '~/commit/pipelines/pipelines_bundle';
import '~/lib/utils/common_utils';
jest.mock('~/lib/utils/webpack', () => ({
diff --git a/spec/frontend/milestones/milestone_combobox_spec.js b/spec/frontend/milestones/milestone_combobox_spec.js
index 8c519abe382..4d1a0a0a440 100644
--- a/spec/frontend/milestones/milestone_combobox_spec.js
+++ b/spec/frontend/milestones/milestone_combobox_spec.js
@@ -1,13 +1,13 @@
-import Vue, { nextTick } from 'vue';
-import Vuex from 'vuex';
+import { GlLoadingIcon, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
-import { GlLoadingIcon, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui';
+import Vue, { nextTick } from 'vue';
+import Vuex from 'vuex';
import { ENTER_KEY } from '~/lib/utils/keys';
import MilestoneCombobox from '~/milestones/components/milestone_combobox.vue';
-import { projectMilestones, groupMilestones } from './mock_data';
import createStore from '~/milestones/stores/';
+import { projectMilestones, groupMilestones } from './mock_data';
const extraLinks = [
{ text: 'Create new', url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/new' },
diff --git a/spec/frontend/milestones/stores/actions_spec.js b/spec/frontend/milestones/stores/actions_spec.js
index a62b0c49a80..4355ea71fb2 100644
--- a/spec/frontend/milestones/stores/actions_spec.js
+++ b/spec/frontend/milestones/stores/actions_spec.js
@@ -1,7 +1,7 @@
import testAction from 'helpers/vuex_action_helper';
-import createState from '~/milestones/stores/state';
import * as actions from '~/milestones/stores/actions';
import * as types from '~/milestones/stores/mutation_types';
+import createState from '~/milestones/stores/state';
let mockProjectMilestonesReturnValue;
let mockGroupMilestonesReturnValue;
diff --git a/spec/frontend/milestones/stores/mutations_spec.js b/spec/frontend/milestones/stores/mutations_spec.js
index 0b69a9d572d..91b2acf23c5 100644
--- a/spec/frontend/milestones/stores/mutations_spec.js
+++ b/spec/frontend/milestones/stores/mutations_spec.js
@@ -1,6 +1,6 @@
-import createState from '~/milestones/stores/state';
-import mutations from '~/milestones/stores/mutations';
import * as types from '~/milestones/stores/mutation_types';
+import mutations from '~/milestones/stores/mutations';
+import createState from '~/milestones/stores/state';
describe('Milestones combobox Vuex store mutations', () => {
let state;
diff --git a/spec/frontend/mini_pipeline_graph_dropdown_spec.js b/spec/frontend/mini_pipeline_graph_dropdown_spec.js
index b21ddabbfb1..3ff34c967e4 100644
--- a/spec/frontend/mini_pipeline_graph_dropdown_spec.js
+++ b/spec/frontend/mini_pipeline_graph_dropdown_spec.js
@@ -1,5 +1,5 @@
-import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter';
+import $ from 'jquery';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown';
diff --git a/spec/frontend/monitoring/alert_widget_spec.js b/spec/frontend/monitoring/alert_widget_spec.js
index fd9fb374274..1f0597bac67 100644
--- a/spec/frontend/monitoring/alert_widget_spec.js
+++ b/spec/frontend/monitoring/alert_widget_spec.js
@@ -1,8 +1,8 @@
-import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon, GlTooltip, GlSprintf, GlBadge } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
-import AlertWidget from '~/monitoring/components/alert_widget.vue';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import AlertWidget from '~/monitoring/components/alert_widget.vue';
const mockReadAlert = jest.fn();
const mockCreateAlert = jest.fn();
diff --git a/spec/frontend/monitoring/components/alert_widget_form_spec.js b/spec/frontend/monitoring/components/alert_widget_form_spec.js
index 6d87fb85f4d..e0ef1040f6b 100644
--- a/spec/frontend/monitoring/components/alert_widget_form_spec.js
+++ b/spec/frontend/monitoring/components/alert_widget_form_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import INVALID_URL from '~/lib/utils/invalid_url';
import AlertWidgetForm from '~/monitoring/components/alert_widget_form.vue';
import ModalStub from '../stubs/modal_stub';
diff --git a/spec/frontend/monitoring/components/charts/anomaly_spec.js b/spec/frontend/monitoring/components/charts/anomaly_spec.js
index dad3003d536..c44fd8dce33 100644
--- a/spec/frontend/monitoring/components/charts/anomaly_spec.js
+++ b/spec/frontend/monitoring/components/charts/anomaly_spec.js
@@ -2,10 +2,10 @@ import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
import Anomaly from '~/monitoring/components/charts/anomaly.vue';
+import MonitorTimeSeriesChart from '~/monitoring/components/charts/time_series.vue';
import { colorValues } from '~/monitoring/constants';
-import { anomalyDeploymentData, mockProjectDir } from '../../mock_data';
import { anomalyGraphData } from '../../graph_data';
-import MonitorTimeSeriesChart from '~/monitoring/components/charts/time_series.vue';
+import { anomalyDeploymentData, mockProjectDir } from '../../mock_data';
const mockProjectPath = `${TEST_HOST}${mockProjectDir}`;
diff --git a/spec/frontend/monitoring/components/charts/bar_spec.js b/spec/frontend/monitoring/components/charts/bar_spec.js
index 40edde5f666..6368c53943a 100644
--- a/spec/frontend/monitoring/components/charts/bar_spec.js
+++ b/spec/frontend/monitoring/components/charts/bar_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlBarChart } from '@gitlab/ui/dist/charts';
+import { shallowMount } from '@vue/test-utils';
import Bar from '~/monitoring/components/charts/bar.vue';
import { barGraphData } from '../../graph_data';
diff --git a/spec/frontend/monitoring/components/charts/column_spec.js b/spec/frontend/monitoring/components/charts/column_spec.js
index 0c4b6e8990d..e10cb3a456a 100644
--- a/spec/frontend/monitoring/components/charts/column_spec.js
+++ b/spec/frontend/monitoring/components/charts/column_spec.js
@@ -1,6 +1,6 @@
+import { GlColumnChart } from '@gitlab/ui/dist/charts';
import { shallowMount } from '@vue/test-utils';
import timezoneMock from 'timezone-mock';
-import { GlColumnChart } from '@gitlab/ui/dist/charts';
import ColumnChart from '~/monitoring/components/charts/column.vue';
jest.mock('~/lib/utils/icon_utils', () => ({
diff --git a/spec/frontend/monitoring/components/charts/gauge_spec.js b/spec/frontend/monitoring/components/charts/gauge_spec.js
index 9215f2e411f..c8f67d5d8c7 100644
--- a/spec/frontend/monitoring/components/charts/gauge_spec.js
+++ b/spec/frontend/monitoring/components/charts/gauge_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlGaugeChart } from '@gitlab/ui/dist/charts';
+import { shallowMount } from '@vue/test-utils';
import GaugeChart from '~/monitoring/components/charts/gauge.vue';
import { gaugeChartGraphData } from '../../graph_data';
diff --git a/spec/frontend/monitoring/components/charts/heatmap_spec.js b/spec/frontend/monitoring/components/charts/heatmap_spec.js
index c8375810a7b..841b7e0648a 100644
--- a/spec/frontend/monitoring/components/charts/heatmap_spec.js
+++ b/spec/frontend/monitoring/components/charts/heatmap_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlHeatmap } from '@gitlab/ui/dist/charts';
+import { shallowMount } from '@vue/test-utils';
import timezoneMock from 'timezone-mock';
import Heatmap from '~/monitoring/components/charts/heatmap.vue';
import { heatmapGraphData } from '../../graph_data';
diff --git a/spec/frontend/monitoring/components/charts/single_stat_spec.js b/spec/frontend/monitoring/components/charts/single_stat_spec.js
index 37712eb3012..8633b49565f 100644
--- a/spec/frontend/monitoring/components/charts/single_stat_spec.js
+++ b/spec/frontend/monitoring/components/charts/single_stat_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlSingleStat } from '@gitlab/ui/dist/charts';
+import { shallowMount } from '@vue/test-utils';
import SingleStatChart from '~/monitoring/components/charts/single_stat.vue';
import { singleStatGraphData } from '../../graph_data';
@@ -27,8 +27,12 @@ describe('Single Stat Chart component', () => {
describe('computed', () => {
describe('statValue', () => {
- it('should interpolate the value and unit props', () => {
- expect(findChart().props('value')).toBe('1.00MB');
+ it('should display the correct value', () => {
+ expect(findChart().props('value')).toBe('1.00');
+ });
+
+ it('should display the correct value unit', () => {
+ expect(findChart().props('unit')).toBe('MB');
});
it('should change the value representation to a percentile one', () => {
@@ -36,7 +40,8 @@ describe('Single Stat Chart component', () => {
graphData: singleStatGraphData({ max_value: 120 }, { value: 91 }),
});
- expect(findChart().props('value')).toContain('75.83%');
+ expect(findChart().props('value')).toBe('75.83');
+ expect(findChart().props('unit')).toBe('%');
});
it('should display NaN for non numeric maxValue values', () => {
diff --git a/spec/frontend/monitoring/components/charts/stacked_column_spec.js b/spec/frontend/monitoring/components/charts/stacked_column_spec.js
index 4d4224a8b11..f47728313c6 100644
--- a/spec/frontend/monitoring/components/charts/stacked_column_spec.js
+++ b/spec/frontend/monitoring/components/charts/stacked_column_spec.js
@@ -1,7 +1,7 @@
+import { GlStackedColumnChart, GlChartLegend } from '@gitlab/ui/dist/charts';
import { shallowMount, mount } from '@vue/test-utils';
-import timezoneMock from 'timezone-mock';
import { cloneDeep } from 'lodash';
-import { GlStackedColumnChart, GlChartLegend } from '@gitlab/ui/dist/charts';
+import timezoneMock from 'timezone-mock';
import StackedColumnChart from '~/monitoring/components/charts/stacked_column.vue';
import { stackedColumnGraphData } from '../../graph_data';
diff --git a/spec/frontend/monitoring/components/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js
index b7e1cb91987..afa63bcff29 100644
--- a/spec/frontend/monitoring/components/charts/time_series_spec.js
+++ b/spec/frontend/monitoring/components/charts/time_series_spec.js
@@ -1,17 +1,18 @@
-import { mount, shallowMount } from '@vue/test-utils';
-import { setTestTimeout } from 'helpers/timeout';
-import timezoneMock from 'timezone-mock';
import { GlLink } from '@gitlab/ui';
-import { TEST_HOST } from 'helpers/test_constants';
import {
GlAreaChart,
GlLineChart,
GlChartSeriesLabel,
GlChartLegend,
} from '@gitlab/ui/dist/charts';
+import { mount, shallowMount } from '@vue/test-utils';
+import timezoneMock from 'timezone-mock';
+import { TEST_HOST } from 'helpers/test_constants';
+import { setTestTimeout } from 'helpers/timeout';
import { shallowWrapperContainsSlotText } from 'helpers/vue_test_utils_helper';
-import { panelTypes, chartHeight } from '~/monitoring/constants';
import TimeSeries from '~/monitoring/components/charts/time_series.vue';
+import { panelTypes, chartHeight } from '~/monitoring/constants';
+import { timeSeriesGraphData } from '../../graph_data';
import {
deploymentData,
mockProjectDir,
@@ -19,8 +20,6 @@ import {
mockFixedTimeRange,
} from '../../mock_data';
-import { timeSeriesGraphData } from '../../graph_data';
-
jest.mock('lodash/throttle', () =>
// this throttle mock executes immediately
jest.fn((func) => {
diff --git a/spec/frontend/monitoring/components/create_dashboard_modal_spec.js b/spec/frontend/monitoring/components/create_dashboard_modal_spec.js
index d1028445638..8202d423ff3 100644
--- a/spec/frontend/monitoring/components/create_dashboard_modal_spec.js
+++ b/spec/frontend/monitoring/components/create_dashboard_modal_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import CreateDashboardModal from '~/monitoring/components/create_dashboard_modal.vue';
describe('Create dashboard modal', () => {
diff --git a/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js b/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
index 43d5937a3a1..6e98ca28071 100644
--- a/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
@@ -1,14 +1,14 @@
-import { shallowMount } from '@vue/test-utils';
import { GlDropdownItem } from '@gitlab/ui';
-import { createStore } from '~/monitoring/stores';
-import { DASHBOARD_PAGE, PANEL_NEW_PAGE } from '~/monitoring/router/constants';
-import { setupAllDashboards, setupStoreWithData } from '../store_utils';
+import { shallowMount } from '@vue/test-utils';
+import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
import { redirectTo } from '~/lib/utils/url_utility';
-import Tracking from '~/tracking';
import ActionsMenu from '~/monitoring/components/dashboard_actions_menu.vue';
-import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
-import { dashboardActionsMenuProps, dashboardGitResponse } from '../mock_data';
+import { DASHBOARD_PAGE, PANEL_NEW_PAGE } from '~/monitoring/router/constants';
+import { createStore } from '~/monitoring/stores';
import * as types from '~/monitoring/stores/mutation_types';
+import Tracking from '~/tracking';
+import { dashboardActionsMenuProps, dashboardGitResponse } from '../mock_data';
+import { setupAllDashboards, setupStoreWithData } from '../store_utils';
jest.mock('~/lib/utils/url_utility', () => ({
redirectTo: jest.fn(),
diff --git a/spec/frontend/monitoring/components/dashboard_header_spec.js b/spec/frontend/monitoring/components/dashboard_header_spec.js
index 32fd9c45e8d..8be7d641953 100644
--- a/spec/frontend/monitoring/components/dashboard_header_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_header_spec.js
@@ -1,20 +1,20 @@
-import { shallowMount } from '@vue/test-utils';
import { GlDropdownItem, GlSearchBoxByType, GlLoadingIcon, GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { redirectTo } from '~/lib/utils/url_utility';
+import ActionsMenu from '~/monitoring/components/dashboard_actions_menu.vue';
+import DashboardHeader from '~/monitoring/components/dashboard_header.vue';
+import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue';
+import RefreshButton from '~/monitoring/components/refresh_button.vue';
import { createStore } from '~/monitoring/stores';
import * as types from '~/monitoring/stores/mutation_types';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
-import RefreshButton from '~/monitoring/components/refresh_button.vue';
-import DashboardHeader from '~/monitoring/components/dashboard_header.vue';
-import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue';
-import ActionsMenu from '~/monitoring/components/dashboard_actions_menu.vue';
-import { setupAllDashboards, setupStoreWithDashboard, setupStoreWithData } from '../store_utils';
import {
environmentData,
dashboardGitResponse,
selfMonitoringDashboardGitResponse,
dashboardHeaderProps,
} from '../mock_data';
-import { redirectTo } from '~/lib/utils/url_utility';
+import { setupAllDashboards, setupStoreWithDashboard, setupStoreWithData } from '../store_utils';
const mockProjectPath = 'https://path/to/project';
diff --git a/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js b/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js
index 08c69701bd2..b794d0c571e 100644
--- a/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js
@@ -1,14 +1,13 @@
-import { shallowMount } from '@vue/test-utils';
import { GlCard, GlForm, GlFormTextarea, GlAlert } from '@gitlab/ui';
-import { createStore } from '~/monitoring/stores';
+import { shallowMount } from '@vue/test-utils';
import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
+import DashboardPanelBuilder from '~/monitoring/components/dashboard_panel_builder.vue';
+import { createStore } from '~/monitoring/stores';
import * as types from '~/monitoring/stores/mutation_types';
+import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
import { metricsDashboardResponse } from '../fixture_data';
import { mockTimeRange } from '../mock_data';
-import DashboardPanelBuilder from '~/monitoring/components/dashboard_panel_builder.vue';
-import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
-
const mockPanel = metricsDashboardResponse.dashboard.panel_groups[0].panels[0];
describe('dashboard invalid url parameters', () => {
diff --git a/spec/frontend/monitoring/components/dashboard_panel_spec.js b/spec/frontend/monitoring/components/dashboard_panel_spec.js
index f64e05d3a2c..a72dbbd0f41 100644
--- a/spec/frontend/monitoring/components/dashboard_panel_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_panel_spec.js
@@ -1,13 +1,32 @@
-import Vuex from 'vuex';
+import { GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
+import Vuex from 'vuex';
import { setTestTimeout } from 'helpers/timeout';
-import { GlDropdownItem } from '@gitlab/ui';
-import invalidUrl from '~/lib/utils/invalid_url';
import axios from '~/lib/utils/axios_utils';
+import invalidUrl from '~/lib/utils/invalid_url';
import AlertWidget from '~/monitoring/components/alert_widget.vue';
+import MonitorAnomalyChart from '~/monitoring/components/charts/anomaly.vue';
+import MonitorBarChart from '~/monitoring/components/charts/bar.vue';
+import MonitorColumnChart from '~/monitoring/components/charts/column.vue';
+import MonitorEmptyChart from '~/monitoring/components/charts/empty_chart.vue';
+import MonitorHeatmapChart from '~/monitoring/components/charts/heatmap.vue';
+import MonitorSingleStatChart from '~/monitoring/components/charts/single_stat.vue';
+import MonitorStackedColumnChart from '~/monitoring/components/charts/stacked_column.vue';
+import MonitorTimeSeriesChart from '~/monitoring/components/charts/time_series.vue';
import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
+import { panelTypes } from '~/monitoring/constants';
+
+import { createStore, monitoringDashboard } from '~/monitoring/stores';
+import { createStore as createEmbedGroupStore } from '~/monitoring/stores/embed_group';
+import { dashboardProps, graphData, graphDataEmpty } from '../fixture_data';
+import {
+ anomalyGraphData,
+ singleStatGraphData,
+ heatmapGraphData,
+ barGraphData,
+} from '../graph_data';
import {
mockAlert,
mockLogsHref,
@@ -16,27 +35,6 @@ import {
mockNamespacedData,
mockTimeRange,
} from '../mock_data';
-import { dashboardProps, graphData, graphDataEmpty } from '../fixture_data';
-import {
- anomalyGraphData,
- singleStatGraphData,
- heatmapGraphData,
- barGraphData,
-} from '../graph_data';
-
-import { panelTypes } from '~/monitoring/constants';
-
-import MonitorEmptyChart from '~/monitoring/components/charts/empty_chart.vue';
-import MonitorTimeSeriesChart from '~/monitoring/components/charts/time_series.vue';
-import MonitorAnomalyChart from '~/monitoring/components/charts/anomaly.vue';
-import MonitorSingleStatChart from '~/monitoring/components/charts/single_stat.vue';
-import MonitorHeatmapChart from '~/monitoring/components/charts/heatmap.vue';
-import MonitorColumnChart from '~/monitoring/components/charts/column.vue';
-import MonitorBarChart from '~/monitoring/components/charts/bar.vue';
-import MonitorStackedColumnChart from '~/monitoring/components/charts/stacked_column.vue';
-
-import { createStore, monitoringDashboard } from '~/monitoring/stores';
-import { createStore as createEmbedGroupStore } from '~/monitoring/stores/embed_group';
const mocks = {
$toast: {
diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js
index db35f1cdde3..5c7042d4cb5 100644
--- a/spec/frontend/monitoring/components/dashboard_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_spec.js
@@ -1,22 +1,29 @@
import { shallowMount, mount } from '@vue/test-utils';
-import VueDraggable from 'vuedraggable';
import MockAdapter from 'axios-mock-adapter';
+import VueDraggable from 'vuedraggable';
import { TEST_HOST } from 'helpers/test_constants';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
import { ESC_KEY } from '~/lib/utils/keys';
import { objectToQuery } from '~/lib/utils/url_utility';
-import axios from '~/lib/utils/axios_utils';
-import { dashboardEmptyStates, metricStates } from '~/monitoring/constants';
import Dashboard from '~/monitoring/components/dashboard.vue';
import DashboardHeader from '~/monitoring/components/dashboard_header.vue';
-import EmptyState from '~/monitoring/components/empty_state.vue';
-import GroupEmptyState from '~/monitoring/components/group_empty_state.vue';
import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
+import EmptyState from '~/monitoring/components/empty_state.vue';
import GraphGroup from '~/monitoring/components/graph_group.vue';
+import GroupEmptyState from '~/monitoring/components/group_empty_state.vue';
import LinksSection from '~/monitoring/components/links_section.vue';
+import { dashboardEmptyStates, metricStates } from '~/monitoring/constants';
import { createStore } from '~/monitoring/stores';
import * as types from '~/monitoring/stores/mutation_types';
import {
+ metricsDashboardViewModel,
+ metricsDashboardPanelCount,
+ dashboardProps,
+} from '../fixture_data';
+import { dashboardGitResponse, storeVariables } from '../mock_data';
+import {
setupAllDashboards,
setupStoreWithDashboard,
setMetricResult,
@@ -24,13 +31,6 @@ import {
setupStoreWithDataForPanelCount,
setupStoreWithLinks,
} from '../store_utils';
-import { dashboardGitResponse, storeVariables } from '../mock_data';
-import {
- metricsDashboardViewModel,
- metricsDashboardPanelCount,
- dashboardProps,
-} from '../fixture_data';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
jest.mock('~/flash');
diff --git a/spec/frontend/monitoring/components/dashboard_template_spec.js b/spec/frontend/monitoring/components/dashboard_template_spec.js
index 8941e57c4ce..4e220d724f4 100644
--- a/spec/frontend/monitoring/components/dashboard_template_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_template_spec.js
@@ -4,8 +4,8 @@ import axios from '~/lib/utils/axios_utils';
import Dashboard from '~/monitoring/components/dashboard.vue';
import DashboardHeader from '~/monitoring/components/dashboard_header.vue';
import { createStore } from '~/monitoring/stores';
-import { setupAllDashboards } from '../store_utils';
import { dashboardProps } from '../fixture_data';
+import { setupAllDashboards } from '../store_utils';
jest.mock('~/lib/utils/url_utility');
diff --git a/spec/frontend/monitoring/components/dashboard_url_time_spec.js b/spec/frontend/monitoring/components/dashboard_url_time_spec.js
index c4630bde32f..9830b6d047f 100644
--- a/spec/frontend/monitoring/components/dashboard_url_time_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_url_time_spec.js
@@ -1,6 +1,7 @@
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
import {
queryToObject,
redirectTo,
@@ -8,14 +9,13 @@ import {
mergeUrlParams,
updateHistory,
} from '~/lib/utils/url_utility';
-import axios from '~/lib/utils/axios_utils';
-import { mockProjectDir } from '../mock_data';
-import { dashboardProps } from '../fixture_data';
import Dashboard from '~/monitoring/components/dashboard.vue';
import DashboardHeader from '~/monitoring/components/dashboard_header.vue';
import { createStore } from '~/monitoring/stores';
import { defaultTimeRange } from '~/vue_shared/constants';
+import { dashboardProps } from '../fixture_data';
+import { mockProjectDir } from '../mock_data';
jest.mock('~/flash');
jest.mock('~/lib/utils/url_utility');
diff --git a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
index 6ed190051ce..c9241834789 100644
--- a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
+++ b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
@@ -1,6 +1,6 @@
-import { nextTick } from 'vue';
-import { shallowMount } from '@vue/test-utils';
import { GlDropdownItem, GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue';
diff --git a/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js b/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js
index 4abb5b2e27b..9672f6a315a 100644
--- a/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js
+++ b/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js
@@ -1,5 +1,5 @@
-import { nextTick } from 'vue';
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import DuplicateDashboardForm from '~/monitoring/components/duplicate_dashboard_form.vue';
import { dashboardGitResponse } from '../mock_data';
diff --git a/spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js b/spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js
index 7e7065da179..1bc89e509b5 100644
--- a/spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js
+++ b/spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js
@@ -1,12 +1,12 @@
-import Vuex from 'vuex';
-import Vue from 'vue';
-import { shallowMount } from '@vue/test-utils';
import { GlAlert, GlLoadingIcon, GlModal } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import Vuex from 'vuex';
import waitForPromises from 'helpers/wait_for_promises';
-import DuplicateDashboardModal from '~/monitoring/components/duplicate_dashboard_modal.vue';
import DuplicateDashboardForm from '~/monitoring/components/duplicate_dashboard_form.vue';
+import DuplicateDashboardModal from '~/monitoring/components/duplicate_dashboard_modal.vue';
import { dashboardGitResponse } from '../mock_data';
diff --git a/spec/frontend/monitoring/components/embeds/embed_group_spec.js b/spec/frontend/monitoring/components/embeds/embed_group_spec.js
index cb06a1a6b64..79b223d96e4 100644
--- a/spec/frontend/monitoring/components/embeds/embed_group_spec.js
+++ b/spec/frontend/monitoring/components/embeds/embed_group_spec.js
@@ -1,6 +1,6 @@
+import { GlButton, GlCard } from '@gitlab/ui';
import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
-import { GlButton, GlCard } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants';
import EmbedGroup from '~/monitoring/components/embeds/embed_group.vue';
import MetricEmbed from '~/monitoring/components/embeds/metric_embed.vue';
diff --git a/spec/frontend/monitoring/components/embeds/metric_embed_spec.js b/spec/frontend/monitoring/components/embeds/metric_embed_spec.js
index 74f265930b1..90647f50b14 100644
--- a/spec/frontend/monitoring/components/embeds/metric_embed_spec.js
+++ b/spec/frontend/monitoring/components/embeds/metric_embed_spec.js
@@ -1,7 +1,7 @@
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
-import { TEST_HOST } from 'helpers/test_constants';
import { setHTMLFixture } from 'helpers/fixtures';
+import { TEST_HOST } from 'helpers/test_constants';
import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
import MetricEmbed from '~/monitoring/components/embeds/metric_embed.vue';
import { groups, initialState, metricsData, metricsWithData } from './mock_data';
diff --git a/spec/frontend/monitoring/components/empty_state_spec.js b/spec/frontend/monitoring/components/empty_state_spec.js
index abb8b21e9f4..1ecb101574b 100644
--- a/spec/frontend/monitoring/components/empty_state_spec.js
+++ b/spec/frontend/monitoring/components/empty_state_spec.js
@@ -1,7 +1,7 @@
-import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
-import { dashboardEmptyStates } from '~/monitoring/constants';
+import { shallowMount } from '@vue/test-utils';
import EmptyState from '~/monitoring/components/empty_state.vue';
+import { dashboardEmptyStates } from '~/monitoring/constants';
function createComponent(props) {
return shallowMount(EmptyState, {
diff --git a/spec/frontend/monitoring/components/graph_group_spec.js b/spec/frontend/monitoring/components/graph_group_spec.js
index c57461c2d09..625dd3f0b33 100644
--- a/spec/frontend/monitoring/components/graph_group_spec.js
+++ b/spec/frontend/monitoring/components/graph_group_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import GraphGroup from '~/monitoring/components/graph_group.vue';
describe('Graph group component', () => {
diff --git a/spec/frontend/monitoring/components/links_section_spec.js b/spec/frontend/monitoring/components/links_section_spec.js
index 2daad77d513..8fc287c50e4 100644
--- a/spec/frontend/monitoring/components/links_section_spec.js
+++ b/spec/frontend/monitoring/components/links_section_spec.js
@@ -1,7 +1,7 @@
-import { shallowMount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
-import { createStore } from '~/monitoring/stores';
+import { shallowMount } from '@vue/test-utils';
import LinksSection from '~/monitoring/components/links_section.vue';
+import { createStore } from '~/monitoring/stores';
describe('Links Section component', () => {
let store;
diff --git a/spec/frontend/monitoring/components/refresh_button_spec.js b/spec/frontend/monitoring/components/refresh_button_spec.js
index a03d29309d4..248cf32d54b 100644
--- a/spec/frontend/monitoring/components/refresh_button_spec.js
+++ b/spec/frontend/monitoring/components/refresh_button_spec.js
@@ -1,8 +1,8 @@
+import { GlDropdown, GlDropdownItem, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Visibility from 'visibilityjs';
-import { GlDropdown, GlDropdownItem, GlButton } from '@gitlab/ui';
-import { createStore } from '~/monitoring/stores';
import RefreshButton from '~/monitoring/components/refresh_button.vue';
+import { createStore } from '~/monitoring/stores';
describe('RefreshButton', () => {
let wrapper;
diff --git a/spec/frontend/monitoring/components/variables/dropdown_field_spec.js b/spec/frontend/monitoring/components/variables/dropdown_field_spec.js
index f5db17ce367..f5ee32e78e6 100644
--- a/spec/frontend/monitoring/components/variables/dropdown_field_spec.js
+++ b/spec/frontend/monitoring/components/variables/dropdown_field_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import DropdownField from '~/monitoring/components/variables/dropdown_field.vue';
describe('Custom variable component', () => {
diff --git a/spec/frontend/monitoring/components/variables/text_field_spec.js b/spec/frontend/monitoring/components/variables/text_field_spec.js
index 99c6facac38..28e02dff4bf 100644
--- a/spec/frontend/monitoring/components/variables/text_field_spec.js
+++ b/spec/frontend/monitoring/components/variables/text_field_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlFormInput } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import TextField from '~/monitoring/components/variables/text_field.vue';
describe('Text variable component', () => {
diff --git a/spec/frontend/monitoring/components/variables_section_spec.js b/spec/frontend/monitoring/components/variables_section_spec.js
index 3097906ee68..6157de0dafe 100644
--- a/spec/frontend/monitoring/components/variables_section_spec.js
+++ b/spec/frontend/monitoring/components/variables_section_spec.js
@@ -1,9 +1,9 @@
import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
-import VariablesSection from '~/monitoring/components/variables_section.vue';
+import { updateHistory, mergeUrlParams } from '~/lib/utils/url_utility';
import DropdownField from '~/monitoring/components/variables/dropdown_field.vue';
import TextField from '~/monitoring/components/variables/text_field.vue';
-import { updateHistory, mergeUrlParams } from '~/lib/utils/url_utility';
+import VariablesSection from '~/monitoring/components/variables_section.vue';
import { createStore } from '~/monitoring/stores';
import { convertVariablesForURL } from '~/monitoring/utils';
import { storeVariables } from '../mock_data';
diff --git a/spec/frontend/monitoring/csv_export_spec.js b/spec/frontend/monitoring/csv_export_spec.js
index eb2a6e40243..42d19c21a7b 100644
--- a/spec/frontend/monitoring/csv_export_spec.js
+++ b/spec/frontend/monitoring/csv_export_spec.js
@@ -1,5 +1,5 @@
-import { timeSeriesGraphData } from './graph_data';
import { graphDataToCsv } from '~/monitoring/csv_export';
+import { timeSeriesGraphData } from './graph_data';
describe('monitoring export_csv', () => {
describe('graphDataToCsv', () => {
diff --git a/spec/frontend/monitoring/fixture_data.js b/spec/frontend/monitoring/fixture_data.js
index 0f3dbaac493..d20a111c701 100644
--- a/spec/frontend/monitoring/fixture_data.js
+++ b/spec/frontend/monitoring/fixture_data.js
@@ -1,7 +1,7 @@
-import { stateAndPropsFromDataset } from '~/monitoring/utils';
-import { mapToDashboardViewModel } from '~/monitoring/stores/utils';
-import { metricStates } from '~/monitoring/constants';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { metricStates } from '~/monitoring/constants';
+import { mapToDashboardViewModel } from '~/monitoring/stores/utils';
+import { stateAndPropsFromDataset } from '~/monitoring/utils';
import { metricsResult } from './mock_data';
diff --git a/spec/frontend/monitoring/graph_data.js b/spec/frontend/monitoring/graph_data.js
index 0b23ee41927..981955efebb 100644
--- a/spec/frontend/monitoring/graph_data.js
+++ b/spec/frontend/monitoring/graph_data.js
@@ -1,5 +1,5 @@
-import { mapPanelToViewModel, normalizeQueryResponseData } from '~/monitoring/stores/utils';
import { panelTypes, metricStates } from '~/monitoring/constants';
+import { mapPanelToViewModel, normalizeQueryResponseData } from '~/monitoring/stores/utils';
const initTime = 1435781450; // "Wed, 01 Jul 2015 20:10:50 GMT"
const intervalSeconds = 120;
diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js
index ca06c96c7d6..29a7c86491d 100644
--- a/spec/frontend/monitoring/mock_data.js
+++ b/spec/frontend/monitoring/mock_data.js
@@ -1,6 +1,6 @@
// The path below needs to be relative because we import the mock-data to karma
-import { TEST_HOST } from '../__helpers__/test_constants';
import invalidUrl from '~/lib/utils/invalid_url';
+import { TEST_HOST } from '../__helpers__/test_constants';
// This import path needs to be relative for now because this mock data is used in
// Karma specs too, where the helpers/test_constants alias can not be resolved
diff --git a/spec/frontend/monitoring/pages/dashboard_page_spec.js b/spec/frontend/monitoring/pages/dashboard_page_spec.js
index 675165e9e56..dbe9cc21ad5 100644
--- a/spec/frontend/monitoring/pages/dashboard_page_spec.js
+++ b/spec/frontend/monitoring/pages/dashboard_page_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
-import { createStore } from '~/monitoring/stores';
-import DashboardPage from '~/monitoring/pages/dashboard_page.vue';
import Dashboard from '~/monitoring/components/dashboard.vue';
+import DashboardPage from '~/monitoring/pages/dashboard_page.vue';
+import { createStore } from '~/monitoring/stores';
import { dashboardProps } from '../fixture_data';
describe('monitoring/pages/dashboard_page', () => {
diff --git a/spec/frontend/monitoring/pages/panel_new_page_spec.js b/spec/frontend/monitoring/pages/panel_new_page_spec.js
index 83365b754d9..c89cbc52bcb 100644
--- a/spec/frontend/monitoring/pages/panel_new_page_spec.js
+++ b/spec/frontend/monitoring/pages/panel_new_page_spec.js
@@ -1,10 +1,9 @@
-import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
-import { DASHBOARD_PAGE, PANEL_NEW_PAGE } from '~/monitoring/router/constants';
-import { createStore } from '~/monitoring/stores';
+import { shallowMount } from '@vue/test-utils';
import DashboardPanelBuilder from '~/monitoring/components/dashboard_panel_builder.vue';
-
import PanelNewPage from '~/monitoring/pages/panel_new_page.vue';
+import { DASHBOARD_PAGE, PANEL_NEW_PAGE } from '~/monitoring/router/constants';
+import { createStore } from '~/monitoring/stores';
const dashboard = 'dashboard.yml';
diff --git a/spec/frontend/monitoring/requests/index_spec.js b/spec/frontend/monitoring/requests/index_spec.js
index 078de5f15d1..b30b1e60575 100644
--- a/spec/frontend/monitoring/requests/index_spec.js
+++ b/spec/frontend/monitoring/requests/index_spec.js
@@ -1,10 +1,10 @@
import MockAdapter from 'axios-mock-adapter';
import { backoffMockImplementation } from 'helpers/backoff_helper';
import axios from '~/lib/utils/axios_utils';
-import statusCodes from '~/lib/utils/http_status';
import * as commonUtils from '~/lib/utils/common_utils';
-import { metricsDashboardResponse } from '../fixture_data';
+import statusCodes from '~/lib/utils/http_status';
import { getDashboard, getPrometheusQueryData } from '~/monitoring/requests';
+import { metricsDashboardResponse } from '../fixture_data';
describe('monitoring metrics_requests', () => {
let mock;
diff --git a/spec/frontend/monitoring/router_spec.js b/spec/frontend/monitoring/router_spec.js
index c30c4c56a6c..b027d60f61e 100644
--- a/spec/frontend/monitoring/router_spec.js
+++ b/spec/frontend/monitoring/router_spec.js
@@ -1,10 +1,10 @@
import { mount, createLocalVue } from '@vue/test-utils';
import VueRouter from 'vue-router';
+import Dashboard from '~/monitoring/components/dashboard.vue';
import DashboardPage from '~/monitoring/pages/dashboard_page.vue';
import PanelNewPage from '~/monitoring/pages/panel_new_page.vue';
-import Dashboard from '~/monitoring/components/dashboard.vue';
-import { createStore } from '~/monitoring/stores';
import createRouter from '~/monitoring/router';
+import { createStore } from '~/monitoring/stores';
import { dashboardProps } from './fixture_data';
import { dashboardHeaderProps } from './mock_data';
diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js
index 319441b5ba2..b7f741c449f 100644
--- a/spec/frontend/monitoring/store/actions_spec.js
+++ b/spec/frontend/monitoring/store/actions_spec.js
@@ -1,17 +1,16 @@
import MockAdapter from 'axios-mock-adapter';
-import testAction from 'helpers/vuex_action_helper';
import { backoffMockImplementation } from 'helpers/backoff_helper';
-import Tracking from '~/tracking';
+import testAction from 'helpers/vuex_action_helper';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import statusCodes from '~/lib/utils/http_status';
import * as commonUtils from '~/lib/utils/common_utils';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
-import { defaultTimeRange } from '~/vue_shared/constants';
-import * as getters from '~/monitoring/stores/getters';
+import statusCodes from '~/lib/utils/http_status';
import { ENVIRONMENT_AVAILABLE_STATE } from '~/monitoring/constants';
+import getAnnotations from '~/monitoring/queries/getAnnotations.query.graphql';
+import getDashboardValidationWarnings from '~/monitoring/queries/getDashboardValidationWarnings.query.graphql';
+import getEnvironments from '~/monitoring/queries/getEnvironments.query.graphql';
import { createStore } from '~/monitoring/stores';
-import * as types from '~/monitoring/stores/mutation_types';
import {
setGettingStartedEmptyState,
setInitialState,
@@ -33,15 +32,21 @@ import {
fetchVariableMetricLabelValues,
fetchPanelPreview,
} from '~/monitoring/stores/actions';
+import * as getters from '~/monitoring/stores/getters';
+import * as types from '~/monitoring/stores/mutation_types';
+import storeState from '~/monitoring/stores/state';
import {
gqClient,
parseEnvironmentsResponse,
parseAnnotationsResponse,
} from '~/monitoring/stores/utils';
-import getEnvironments from '~/monitoring/queries/getEnvironments.query.graphql';
-import getAnnotations from '~/monitoring/queries/getAnnotations.query.graphql';
-import getDashboardValidationWarnings from '~/monitoring/queries/getDashboardValidationWarnings.query.graphql';
-import storeState from '~/monitoring/stores/state';
+import Tracking from '~/tracking';
+import { defaultTimeRange } from '~/vue_shared/constants';
+import {
+ metricsDashboardResponse,
+ metricsDashboardViewModel,
+ metricsDashboardPanelCount,
+} from '../fixture_data';
import {
deploymentData,
environmentData,
@@ -49,11 +54,6 @@ import {
dashboardGitResponse,
mockDashboardsErrorResponse,
} from '../mock_data';
-import {
- metricsDashboardResponse,
- metricsDashboardViewModel,
- metricsDashboardPanelCount,
-} from '../fixture_data';
jest.mock('~/flash');
diff --git a/spec/frontend/monitoring/store/embed_group/mutations_spec.js b/spec/frontend/monitoring/store/embed_group/mutations_spec.js
index a1d04e23e41..2f8d7687aad 100644
--- a/spec/frontend/monitoring/store/embed_group/mutations_spec.js
+++ b/spec/frontend/monitoring/store/embed_group/mutations_spec.js
@@ -1,6 +1,6 @@
-import state from '~/monitoring/stores/embed_group/state';
-import mutations from '~/monitoring/stores/embed_group/mutations';
import * as types from '~/monitoring/stores/embed_group/mutation_types';
+import mutations from '~/monitoring/stores/embed_group/mutations';
+import state from '~/monitoring/stores/embed_group/state';
import { mockNamespace } from '../../mock_data';
describe('Embed group mutations', () => {
diff --git a/spec/frontend/monitoring/store/getters_spec.js b/spec/frontend/monitoring/store/getters_spec.js
index 771ec0ea549..c7f3bdbf1f8 100644
--- a/spec/frontend/monitoring/store/getters_spec.js
+++ b/spec/frontend/monitoring/store/getters_spec.js
@@ -1,8 +1,9 @@
import _ from 'lodash';
+import { metricStates } from '~/monitoring/constants';
import * as getters from '~/monitoring/stores/getters';
-import mutations from '~/monitoring/stores/mutations';
import * as types from '~/monitoring/stores/mutation_types';
-import { metricStates } from '~/monitoring/constants';
+import mutations from '~/monitoring/stores/mutations';
+import { metricsDashboardPayload } from '../fixture_data';
import {
customDashboardBasePath,
environmentData,
@@ -11,7 +12,6 @@ import {
storeVariables,
mockLinks,
} from '../mock_data';
-import { metricsDashboardPayload } from '../fixture_data';
describe('Monitoring store Getters', () => {
let state;
diff --git a/spec/frontend/monitoring/store/mutations_spec.js b/spec/frontend/monitoring/store/mutations_spec.js
index 571828eb67c..ae1a4e16b30 100644
--- a/spec/frontend/monitoring/store/mutations_spec.js
+++ b/spec/frontend/monitoring/store/mutations_spec.js
@@ -1,12 +1,12 @@
import httpStatusCodes from '~/lib/utils/http_status';
-import mutations from '~/monitoring/stores/mutations';
+import { dashboardEmptyStates, metricStates } from '~/monitoring/constants';
import * as types from '~/monitoring/stores/mutation_types';
+import mutations from '~/monitoring/stores/mutations';
import state from '~/monitoring/stores/state';
-import { dashboardEmptyStates, metricStates } from '~/monitoring/constants';
-import { deploymentData, dashboardGitResponse, storeTextVariables } from '../mock_data';
-import { prometheusMatrixMultiResult } from '../graph_data';
import { metricsDashboardPayload } from '../fixture_data';
+import { prometheusMatrixMultiResult } from '../graph_data';
+import { deploymentData, dashboardGitResponse, storeTextVariables } from '../mock_data';
describe('Monitoring mutations', () => {
let stateCopy;
diff --git a/spec/frontend/monitoring/store/utils_spec.js b/spec/frontend/monitoring/store/utils_spec.js
index 58bb87cb332..697bdb9185f 100644
--- a/spec/frontend/monitoring/store/utils_spec.js
+++ b/spec/frontend/monitoring/store/utils_spec.js
@@ -1,4 +1,6 @@
import { SUPPORTED_FORMATS } from '~/lib/utils/unit_format';
+import * as urlUtils from '~/lib/utils/url_utility';
+import { NOT_IN_DB_PREFIX } from '~/monitoring/constants';
import {
uniqMetricsId,
parseEnvironmentsResponse,
@@ -10,9 +12,7 @@ import {
addDashboardMetaDataToLink,
normalizeCustomDashboardPath,
} from '~/monitoring/stores/utils';
-import * as urlUtils from '~/lib/utils/url_utility';
import { annotationsData } from '../mock_data';
-import { NOT_IN_DB_PREFIX } from '~/monitoring/constants';
const projectPath = 'gitlab-org/gitlab-test';
diff --git a/spec/frontend/monitoring/store/variable_mapping_spec.js b/spec/frontend/monitoring/store/variable_mapping_spec.js
index de124b0313c..58e7175c04c 100644
--- a/spec/frontend/monitoring/store/variable_mapping_spec.js
+++ b/spec/frontend/monitoring/store/variable_mapping_spec.js
@@ -1,3 +1,4 @@
+import * as urlUtils from '~/lib/utils/url_utility';
import {
parseTemplatingVariables,
mergeURLVariables,
@@ -9,7 +10,6 @@ import {
storeCustomVariables,
storeMetricLabelValuesVariables,
} from '../mock_data';
-import * as urlUtils from '~/lib/utils/url_utility';
describe('Monitoring variable mapping', () => {
describe('parseTemplatingVariables', () => {
diff --git a/spec/frontend/monitoring/store_utils.js b/spec/frontend/monitoring/store_utils.js
index 911ccc78f7b..96219661b9b 100644
--- a/spec/frontend/monitoring/store_utils.js
+++ b/spec/frontend/monitoring/store_utils.js
@@ -1,6 +1,6 @@
import * as types from '~/monitoring/stores/mutation_types';
-import { metricsResult, environmentData, dashboardGitResponse } from './mock_data';
import { metricsDashboardPayload } from './fixture_data';
+import { metricsResult, environmentData, dashboardGitResponse } from './mock_data';
export const setMetricResult = ({ store, result, group = 0, panel = 0, metric = 0 }) => {
const { dashboard } = store.state.monitoringDashboard;
diff --git a/spec/frontend/monitoring/utils_spec.js b/spec/frontend/monitoring/utils_spec.js
index cd49e4c7968..25ae4dcd702 100644
--- a/spec/frontend/monitoring/utils_spec.js
+++ b/spec/frontend/monitoring/utils_spec.js
@@ -1,9 +1,9 @@
import { TEST_HOST } from 'helpers/test_constants';
-import * as monitoringUtils from '~/monitoring/utils';
import * as urlUtils from '~/lib/utils/url_utility';
-import { mockProjectDir, barMockData } from './mock_data';
-import { singleStatGraphData, anomalyGraphData } from './graph_data';
+import * as monitoringUtils from '~/monitoring/utils';
import { metricsDashboardViewModel, graphData } from './fixture_data';
+import { singleStatGraphData, anomalyGraphData } from './graph_data';
+import { mockProjectDir, barMockData } from './mock_data';
const mockPath = `${TEST_HOST}${mockProjectDir}/-/environments/29/metrics`;
diff --git a/spec/frontend/namespace_select_spec.js b/spec/frontend/namespace_select_spec.js
index d6f3eb75cd9..a38508dd601 100644
--- a/spec/frontend/namespace_select_spec.js
+++ b/spec/frontend/namespace_select_spec.js
@@ -1,5 +1,5 @@
-import NamespaceSelect from '~/namespace_select';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import NamespaceSelect from '~/namespace_select';
jest.mock('~/deprecated_jquery_dropdown');
diff --git a/spec/frontend/notebook/cells/markdown_spec.js b/spec/frontend/notebook/cells/markdown_spec.js
index ad33858da22..4d6addaf47c 100644
--- a/spec/frontend/notebook/cells/markdown_spec.js
+++ b/spec/frontend/notebook/cells/markdown_spec.js
@@ -1,5 +1,5 @@
-import Vue from 'vue';
import katex from 'katex';
+import Vue from 'vue';
import MarkdownComponent from '~/notebook/cells/markdown.vue';
const Component = Vue.extend(MarkdownComponent);
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js
index 002c4f206cb..2f58f75ab70 100644
--- a/spec/frontend/notes/components/comment_form_spec.js
+++ b/spec/frontend/notes/components/comment_form_spec.js
@@ -1,15 +1,15 @@
-import { nextTick } from 'vue';
import { mount, shallowMount } from '@vue/test-utils';
-import MockAdapter from 'axios-mock-adapter';
import Autosize from 'autosize';
+import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import { deprecatedCreateFlash as flash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import createStore from '~/notes/stores';
import CommentForm from '~/notes/components/comment_form.vue';
import * as constants from '~/notes/constants';
import eventHub from '~/notes/event_hub';
-import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import createStore from '~/notes/stores';
import { loggedOutnoteableData, notesDataMock, userDataMock, noteableDataMock } from '../mock_data';
jest.mock('autosize');
@@ -22,11 +22,25 @@ describe('issue_comment_form component', () => {
let wrapper;
let axiosMock;
- const findCloseReopenButton = () => wrapper.find('[data-testid="close-reopen-button"]');
+ const findCloseReopenButton = () => wrapper.findByTestId('close-reopen-button');
+ const findCommentButton = () => wrapper.findByTestId('comment-button');
+ const findTextArea = () => wrapper.findByTestId('comment-field');
+ const findConfidentialNoteCheckbox = () => wrapper.findByTestId('confidential-note-checkbox');
- const findCommentButton = () => wrapper.find('[data-testid="comment-button"]');
+ const createNotableDataMock = (data = {}) => {
+ return {
+ ...noteableDataMock,
+ ...data,
+ };
+ };
- const findTextArea = () => wrapper.find('[data-testid="comment-field"]');
+ const notableDataMockCanUpdateIssuable = createNotableDataMock({
+ current_user: { can_update: true, can_create_note: true },
+ });
+
+ const notableDataMockCannotUpdateIssuable = createNotableDataMock({
+ current_user: { can_update: false, can_create_note: true },
+ });
const mountComponent = ({
initialData = {},
@@ -34,23 +48,29 @@ describe('issue_comment_form component', () => {
noteableData = noteableDataMock,
notesData = notesDataMock,
userData = userDataMock,
+ features = {},
mountFunction = shallowMount,
} = {}) => {
store.dispatch('setNoteableData', noteableData);
store.dispatch('setNotesData', notesData);
store.dispatch('setUserData', userData);
- wrapper = mountFunction(CommentForm, {
- propsData: {
- noteableType,
- },
- data() {
- return {
- ...initialData,
- };
- },
- store,
- });
+ wrapper = extendedWrapper(
+ mountFunction(CommentForm, {
+ propsData: {
+ noteableType,
+ },
+ data() {
+ return {
+ ...initialData,
+ };
+ },
+ store,
+ provide: {
+ glFeatures: features,
+ },
+ }),
+ );
};
beforeEach(() => {
@@ -64,14 +84,6 @@ describe('issue_comment_form component', () => {
});
describe('user is logged in', () => {
- describe('avatar', () => {
- it('should render user avatar with link', () => {
- mountComponent({ mountFunction: mount });
-
- expect(wrapper.find(UserAvatarLink).attributes('href')).toBe(userDataMock.path);
- });
- });
-
describe('handleSave', () => {
it('should request to save note when note is entered', () => {
mountComponent({ mountFunction: mount, initialData: { note: 'hello world' } });
@@ -368,6 +380,83 @@ describe('issue_comment_form component', () => {
});
});
});
+
+ describe('confidential notes checkbox', () => {
+ describe('when confidentialNotes feature flag is `false`', () => {
+ const features = { confidentialNotes: false };
+
+ it('should not render checkbox', () => {
+ mountComponent({
+ mountFunction: mount,
+ initialData: { note: 'confidential note' },
+ noteableData: { ...notableDataMockCanUpdateIssuable },
+ features,
+ });
+
+ const checkbox = findConfidentialNoteCheckbox();
+ expect(checkbox.exists()).toBe(false);
+ });
+ });
+
+ describe('when confidentialNotes feature flag is `true`', () => {
+ const features = { confidentialNotes: true };
+
+ it('should render checkbox as unchecked by default', () => {
+ mountComponent({
+ mountFunction: mount,
+ initialData: { note: 'confidential note' },
+ noteableData: { ...notableDataMockCanUpdateIssuable },
+ features,
+ });
+
+ const checkbox = findConfidentialNoteCheckbox();
+ expect(checkbox.exists()).toBe(true);
+ expect(checkbox.element.checked).toBe(false);
+ });
+
+ describe.each`
+ shouldCheckboxBeChecked
+ ${true}
+ ${false}
+ `('when checkbox value is `$shouldCheckboxBeChecked`', ({ shouldCheckboxBeChecked }) => {
+ it(`sets \`confidential\` to \`${shouldCheckboxBeChecked}\``, async () => {
+ mountComponent({
+ mountFunction: mount,
+ initialData: { note: 'confidential note' },
+ noteableData: { ...notableDataMockCanUpdateIssuable },
+ features,
+ });
+
+ jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue({});
+
+ const checkbox = findConfidentialNoteCheckbox();
+
+ // check checkbox
+ checkbox.element.checked = shouldCheckboxBeChecked;
+ checkbox.trigger('change');
+ await wrapper.vm.$nextTick();
+
+ // submit comment
+ wrapper.findByTestId('comment-button').trigger('click');
+
+ const [providedData] = wrapper.vm.saveNote.mock.calls[0];
+ expect(providedData.data.note.confidential).toBe(shouldCheckboxBeChecked);
+ });
+ });
+
+ describe('when user cannot update issuable', () => {
+ it('should not render checkbox', () => {
+ mountComponent({
+ mountFunction: mount,
+ noteableData: { ...notableDataMockCannotUpdateIssuable },
+ features,
+ });
+
+ expect(findConfidentialNoteCheckbox().exists()).toBe(false);
+ });
+ });
+ });
+ });
});
describe('user is not logged in', () => {
diff --git a/spec/frontend/notes/components/diff_discussion_header_spec.js b/spec/frontend/notes/components/diff_discussion_header_spec.js
index 3940439a32b..fdc89522901 100644
--- a/spec/frontend/notes/components/diff_discussion_header_spec.js
+++ b/spec/frontend/notes/components/diff_discussion_header_spec.js
@@ -1,10 +1,10 @@
import { mount } from '@vue/test-utils';
-import createStore from '~/notes/stores';
import diffDiscussionHeader from '~/notes/components/diff_discussion_header.vue';
+import createStore from '~/notes/stores';
-import { discussionMock } from '../mock_data';
import mockDiffFile from '../../diffs/mock_data/diff_discussions';
+import { discussionMock } from '../mock_data';
const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json';
diff --git a/spec/frontend/notes/components/diff_with_note_spec.js b/spec/frontend/notes/components/diff_with_note_spec.js
index 6480af015db..e997fc4da50 100644
--- a/spec/frontend/notes/components/diff_with_note_spec.js
+++ b/spec/frontend/notes/components/diff_with_note_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import DiffWithNote from '~/notes/components/diff_with_note.vue';
import { createStore } from '~/mr_notes/stores';
+import DiffWithNote from '~/notes/components/diff_with_note.vue';
const discussionFixture = 'merge_requests/diff_discussion.json';
const imageDiscussionFixture = 'merge_requests/image_diff_discussion.json';
diff --git a/spec/frontend/notes/components/discussion_actions_spec.js b/spec/frontend/notes/components/discussion_actions_spec.js
index 48e569720e9..03e5842bb0f 100644
--- a/spec/frontend/notes/components/discussion_actions_spec.js
+++ b/spec/frontend/notes/components/discussion_actions_spec.js
@@ -1,10 +1,10 @@
import { shallowMount, mount } from '@vue/test-utils';
-import { discussionMock } from '../mock_data';
import DiscussionActions from '~/notes/components/discussion_actions.vue';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
import ResolveDiscussionButton from '~/notes/components/discussion_resolve_button.vue';
import ResolveWithIssueButton from '~/notes/components/discussion_resolve_with_issue_button.vue';
import createStore from '~/notes/stores';
+import { discussionMock } from '../mock_data';
// NOTE: clone mock_data so that it is not accidentally mutated
const createDiscussionMock = (props = {}) =>
diff --git a/spec/frontend/notes/components/discussion_counter_spec.js b/spec/frontend/notes/components/discussion_counter_spec.js
index ebf7d52f38b..9db0f823d84 100644
--- a/spec/frontend/notes/components/discussion_counter_spec.js
+++ b/spec/frontend/notes/components/discussion_counter_spec.js
@@ -1,10 +1,10 @@
-import Vuex from 'vuex';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
-import notesModule from '~/notes/stores/modules';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
import DiscussionCounter from '~/notes/components/discussion_counter.vue';
-import { noteableDataMock, discussionMock, notesDataMock, userDataMock } from '../mock_data';
+import notesModule from '~/notes/stores/modules';
import * as types from '~/notes/stores/mutation_types';
+import { noteableDataMock, discussionMock, notesDataMock, userDataMock } from '../mock_data';
describe('DiscussionCounter component', () => {
let store;
diff --git a/spec/frontend/notes/components/discussion_filter_note_spec.js b/spec/frontend/notes/components/discussion_filter_note_spec.js
index 9ae3f08df77..ad9a2e898eb 100644
--- a/spec/frontend/notes/components/discussion_filter_note_spec.js
+++ b/spec/frontend/notes/components/discussion_filter_note_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlButton, GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import DiscussionFilterNote from '~/notes/components/discussion_filter_note.vue';
import eventHub from '~/notes/event_hub';
diff --git a/spec/frontend/notes/components/discussion_filter_spec.js b/spec/frontend/notes/components/discussion_filter_spec.js
index aeba8e8056c..6f62b8ba528 100644
--- a/spec/frontend/notes/components/discussion_filter_spec.js
+++ b/spec/frontend/notes/components/discussion_filter_spec.js
@@ -1,13 +1,13 @@
-import Vuex from 'vuex';
import { createLocalVue, mount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
+import Vuex from 'vuex';
import { TEST_HOST } from 'helpers/test_constants';
import createEventHub from '~/helpers/event_hub_factory';
import axios from '~/lib/utils/axios_utils';
-import notesModule from '~/notes/stores/modules';
import DiscussionFilter from '~/notes/components/discussion_filter.vue';
import { DISCUSSION_FILTERS_DEFAULT_VALUE, DISCUSSION_FILTER_TYPES } from '~/notes/constants';
+import notesModule from '~/notes/stores/modules';
import { discussionFiltersMock, discussionMock } from '../mock_data';
diff --git a/spec/frontend/notes/components/discussion_navigator_spec.js b/spec/frontend/notes/components/discussion_navigator_spec.js
index 122814b8e3f..4d55eee2ffa 100644
--- a/spec/frontend/notes/components/discussion_navigator_spec.js
+++ b/spec/frontend/notes/components/discussion_navigator_spec.js
@@ -1,7 +1,7 @@
/* global Mousetrap */
import 'mousetrap';
-import Vue from 'vue';
import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vue from 'vue';
import DiscussionNavigator from '~/notes/components/discussion_navigator.vue';
import eventHub from '~/notes/event_hub';
diff --git a/spec/frontend/notes/components/discussion_notes_spec.js b/spec/frontend/notes/components/discussion_notes_spec.js
index e803dcb7b4a..cd24b9afbdf 100644
--- a/spec/frontend/notes/components/discussion_notes_spec.js
+++ b/spec/frontend/notes/components/discussion_notes_spec.js
@@ -1,13 +1,13 @@
-import { shallowMount } from '@vue/test-utils';
import { getByRole } from '@testing-library/dom';
+import { shallowMount } from '@vue/test-utils';
import '~/behaviors/markdown/render_gfm';
-import { SYSTEM_NOTE } from '~/notes/constants';
import DiscussionNotes from '~/notes/components/discussion_notes.vue';
import NoteableNote from '~/notes/components/noteable_note.vue';
+import { SYSTEM_NOTE } from '~/notes/constants';
+import createStore from '~/notes/stores';
import PlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
import SystemNote from '~/vue_shared/components/notes/system_note.vue';
-import createStore from '~/notes/stores';
import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data';
const LINE_RANGE = {};
@@ -23,7 +23,7 @@ describe('DiscussionNotes', () => {
let wrapper;
const getList = () => getByRole(wrapper.element, 'list');
- const createComponent = (props, features = {}) => {
+ const createComponent = (props) => {
wrapper = shallowMount(DiscussionNotes, {
store,
propsData: {
@@ -38,9 +38,6 @@ describe('DiscussionNotes', () => {
slots: {
'avatar-badge': '<span class="avatar-badge-slot-content" />',
},
- provide: {
- glFeatures: { multilineComments: true, ...features },
- },
});
};
@@ -177,16 +174,14 @@ describe('DiscussionNotes', () => {
});
describe.each`
- desc | props | features | event | expectedCalls
- ${'with `discussion.position`'} | ${{ discussion: DISCUSSION_WITH_LINE_RANGE }} | ${{}} | ${'mouseenter'} | ${[['setSelectedCommentPositionHover', LINE_RANGE]]}
- ${'with `discussion.position`'} | ${{ discussion: DISCUSSION_WITH_LINE_RANGE }} | ${{}} | ${'mouseleave'} | ${[['setSelectedCommentPositionHover']]}
- ${'with `discussion.position`'} | ${{ discussion: DISCUSSION_WITH_LINE_RANGE }} | ${{ multilineComments: false }} | ${'mouseenter'} | ${[]}
- ${'with `discussion.position`'} | ${{ discussion: DISCUSSION_WITH_LINE_RANGE }} | ${{ multilineComments: false }} | ${'mouseleave'} | ${[]}
- ${'without `discussion.position`'} | ${{}} | ${{}} | ${'mouseenter'} | ${[]}
- ${'without `discussion.position`'} | ${{}} | ${{}} | ${'mouseleave'} | ${[]}
- `('$desc and features $features', ({ props, event, features, expectedCalls }) => {
+ desc | props | event | expectedCalls
+ ${'with `discussion.position`'} | ${{ discussion: DISCUSSION_WITH_LINE_RANGE }} | ${'mouseenter'} | ${[['setSelectedCommentPositionHover', LINE_RANGE]]}
+ ${'with `discussion.position`'} | ${{ discussion: DISCUSSION_WITH_LINE_RANGE }} | ${'mouseleave'} | ${[['setSelectedCommentPositionHover']]}
+ ${'without `discussion.position`'} | ${{}} | ${'mouseenter'} | ${[]}
+ ${'without `discussion.position`'} | ${{}} | ${'mouseleave'} | ${[]}
+ `('$desc', ({ props, event, expectedCalls }) => {
beforeEach(() => {
- createComponent(props, features);
+ createComponent(props);
jest.spyOn(store, 'dispatch');
});
diff --git a/spec/frontend/notes/components/discussion_resolve_button_spec.js b/spec/frontend/notes/components/discussion_resolve_button_spec.js
index 5105e1013d3..64e061830b9 100644
--- a/spec/frontend/notes/components/discussion_resolve_button_spec.js
+++ b/spec/frontend/notes/components/discussion_resolve_button_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import resolveDiscussionButton from '~/notes/components/discussion_resolve_button.vue';
const buttonTitle = 'Resolve discussion';
diff --git a/spec/frontend/notes/components/multiline_comment_form_spec.js b/spec/frontend/notes/components/multiline_comment_form_spec.js
index 081fd6e10ef..b6d603c6358 100644
--- a/spec/frontend/notes/components/multiline_comment_form_spec.js
+++ b/spec/frontend/notes/components/multiline_comment_form_spec.js
@@ -1,7 +1,7 @@
+import { GlFormSelect } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
-import { mount } from '@vue/test-utils';
-import { GlFormSelect } from '@gitlab/ui';
import MultilineCommentForm from '~/notes/components/multiline_comment_form.vue';
import notesModule from '~/notes/stores/modules';
diff --git a/spec/frontend/notes/components/note_actions/reply_button_spec.js b/spec/frontend/notes/components/note_actions/reply_button_spec.js
index 720ab10b270..4993ded365d 100644
--- a/spec/frontend/notes/components/note_actions/reply_button_spec.js
+++ b/spec/frontend/notes/components/note_actions/reply_button_spec.js
@@ -1,29 +1,22 @@
-import Vuex from 'vuex';
-import { createLocalVue, mount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import ReplyButton from '~/notes/components/note_actions/reply_button.vue';
-const localVue = createLocalVue();
-localVue.use(Vuex);
-
describe('ReplyButton', () => {
let wrapper;
beforeEach(() => {
- wrapper = mount(localVue.extend(ReplyButton), {
- localVue,
- });
+ wrapper = shallowMount(ReplyButton);
});
afterEach(() => {
wrapper.destroy();
+ wrapper = null;
});
it('emits startReplying on click', () => {
- const button = wrapper.find({ ref: 'button' });
-
- button.trigger('click');
+ wrapper.find(GlButton).vm.$emit('click');
- expect(wrapper.emitted().startReplying).toBeTruthy();
- expect(wrapper.emitted().startReplying.length).toBe(1);
+ expect(wrapper.emitted('startReplying')).toEqual([[]]);
});
});
diff --git a/spec/frontend/notes/components/note_actions_spec.js b/spec/frontend/notes/components/note_actions_spec.js
index 3cfc1445cb8..17717ebd09a 100644
--- a/spec/frontend/notes/components/note_actions_spec.js
+++ b/spec/frontend/notes/components/note_actions_spec.js
@@ -1,11 +1,12 @@
-import Vue from 'vue';
import { mount, createLocalVue, createWrapper } from '@vue/test-utils';
-import { TEST_HOST } from 'spec/test_constants';
import AxiosMockAdapter from 'axios-mock-adapter';
-import createStore from '~/notes/stores';
+import Vue from 'vue';
+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 createStore from '~/notes/stores';
import { userDataMock } from '../mock_data';
-import axios from '~/lib/utils/axios_utils';
describe('noteActions', () => {
let wrapper;
@@ -135,7 +136,7 @@ describe('noteActions', () => {
.then(() => {
const emitted = Object.keys(rootWrapper.emitted());
- expect(emitted).toEqual(['bv::hide::tooltip']);
+ expect(emitted).toEqual([BV_HIDE_TOOLTIP]);
done();
})
.catch(done.fail);
diff --git a/spec/frontend/notes/components/note_awards_list_spec.js b/spec/frontend/notes/components/note_awards_list_spec.js
index 13a817902e6..9fc89ffa473 100644
--- a/spec/frontend/notes/components/note_awards_list_spec.js
+++ b/spec/frontend/notes/components/note_awards_list_spec.js
@@ -1,9 +1,9 @@
-import Vue from 'vue';
import AxiosMockAdapter from 'axios-mock-adapter';
+import Vue from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
-import createStore from '~/notes/stores';
import awardsNote from '~/notes/components/note_awards_list.vue';
+import createStore from '~/notes/stores';
import { noteableDataMock, notesDataMock } from '../mock_data';
describe('note_awards_list component', () => {
diff --git a/spec/frontend/notes/components/note_body_spec.js b/spec/frontend/notes/components/note_body_spec.js
index 3c11c266f90..4922de987fa 100644
--- a/spec/frontend/notes/components/note_body_spec.js
+++ b/spec/frontend/notes/components/note_body_spec.js
@@ -1,6 +1,14 @@
+import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
-import createStore from '~/notes/stores';
+import Vuex from 'vuex';
+
+import { suggestionCommitMessage } from '~/diffs/store/getters';
import noteBody from '~/notes/components/note_body.vue';
+import createStore from '~/notes/stores';
+import notes from '~/notes/stores/modules/index';
+
+import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
+
import { noteableDataMock, notesDataMock, note } from '../mock_data';
describe('issue_note_body component', () => {
@@ -54,4 +62,50 @@ describe('issue_note_body component', () => {
expect(vm.autosave.key).toEqual(autosaveKey);
});
});
+
+ describe('commitMessage', () => {
+ let wrapper;
+
+ Vue.use(Vuex);
+
+ beforeEach(() => {
+ const notesStore = notes();
+
+ notesStore.state.notes = {};
+
+ store = new Vuex.Store({
+ modules: {
+ notes: notesStore,
+ diffs: {
+ namespaced: true,
+ state: {
+ defaultSuggestionCommitMessage:
+ '%{branch_name}%{project_path}%{project_name}%{username}%{user_full_name}%{file_paths}%{suggestions_count}%{files_count}',
+ branchName: 'branch',
+ projectPath: '/path',
+ projectName: 'name',
+ username: 'user',
+ userFullName: 'user userton',
+ },
+ getters: { suggestionCommitMessage },
+ },
+ },
+ });
+
+ wrapper = shallowMount(noteBody, {
+ store,
+ propsData: {
+ note: { ...note, suggestions: [12345] },
+ canEdit: true,
+ file: { file_path: 'abc' },
+ },
+ });
+ });
+
+ it('passes the correct default placeholder commit message for a suggestion to the suggestions component', () => {
+ const commitMessage = wrapper.find(Suggestions).attributes('defaultcommitmessage');
+
+ expect(commitMessage).toBe('branch/pathnameuseruser usertonabc11');
+ });
+ });
});
diff --git a/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js
index e64a75bede9..7615f3b70f1 100644
--- a/spec/frontend/notes/components/note_form_spec.js
+++ b/spec/frontend/notes/components/note_form_spec.js
@@ -1,13 +1,12 @@
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import createStore from '~/notes/stores';
-import NoteForm from '~/notes/components/note_form.vue';
import batchComments from '~/batch_comments/stores/modules/batch_comments';
+import { getDraft, updateDraft } from '~/lib/utils/autosave';
+import NoteForm from '~/notes/components/note_form.vue';
+import createStore from '~/notes/stores';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import { noteableDataMock, notesDataMock, discussionMock } from '../mock_data';
-import { getDraft, updateDraft } from '~/lib/utils/autosave';
-
jest.mock('~/lib/utils/autosave');
describe('issue_note_form component', () => {
@@ -25,6 +24,8 @@ describe('issue_note_form component', () => {
});
};
+ const findCancelButton = () => wrapper.find('[data-testid="cancel"]');
+
beforeEach(() => {
getDraft.mockImplementation((key) => {
if (key === dummyAutosaveKey) {
@@ -160,8 +161,8 @@ describe('issue_note_form component', () => {
});
await nextTick();
- const cancelButton = wrapper.find('[data-testid="cancel"]');
- cancelButton.trigger('click');
+ const cancelButton = findCancelButton();
+ cancelButton.vm.$emit('click');
await nextTick();
expect(wrapper.emitted().cancelForm).toHaveLength(1);
@@ -177,7 +178,7 @@ describe('issue_note_form component', () => {
const textarea = wrapper.find('textarea');
textarea.setValue('Foo');
const saveButton = wrapper.find('.js-vue-issue-save');
- saveButton.trigger('click');
+ saveButton.vm.$emit('click');
expect(wrapper.vm.isSubmitting).toBe(true);
});
@@ -272,7 +273,7 @@ describe('issue_note_form component', () => {
await nextTick();
const cancelButton = wrapper.find('[data-testid="cancelBatchCommentsEnabled"]');
- cancelButton.trigger('click');
+ cancelButton.vm.$emit('click');
expect(wrapper.vm.cancelHandler).toHaveBeenCalledWith(true);
});
@@ -302,16 +303,16 @@ describe('issue_note_form component', () => {
expect(wrapper.find('.js-resolve-checkbox').exists()).toBe(false);
});
- it('hides actions for commits', () => {
+ it('hides actions for commits', async () => {
wrapper.setProps({ discussion: { for_commit: true } });
- return nextTick(() => {
- expect(wrapper.find('.note-form-actions').text()).not.toContain('Start a review');
- });
+ await nextTick();
+
+ expect(wrapper.find('.note-form-actions').text()).not.toContain('Start a review');
});
describe('on enter', () => {
- it('should start review or add to review when cmd+enter is pressed', () => {
+ it('should start review or add to review when cmd+enter is pressed', async () => {
const textarea = wrapper.find('textarea');
jest.spyOn(wrapper.vm, 'handleAddToReview');
@@ -319,9 +320,8 @@ describe('issue_note_form component', () => {
textarea.setValue('Foo');
textarea.trigger('keydown.enter', { metaKey: true });
- return nextTick(() => {
- expect(wrapper.vm.handleAddToReview).toHaveBeenCalled();
- });
+ await nextTick();
+ expect(wrapper.vm.handleAddToReview).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/notes/components/note_header_spec.js b/spec/frontend/notes/components/note_header_spec.js
index 132e3d8aa7e..774d5aaa7d3 100644
--- a/spec/frontend/notes/components/note_header_spec.js
+++ b/spec/frontend/notes/components/note_header_spec.js
@@ -1,9 +1,10 @@
+import { GlSprintf } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue';
import Vuex from 'vuex';
-import { GlSprintf } from '@gitlab/ui';
import NoteHeader from '~/notes/components/note_header.vue';
import { AVAILABILITY_STATUS } from '~/set_status_modal/utils';
+import UserNameWithStatus from '~/sidebar/components/assignees/user_name_with_status.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -36,9 +37,7 @@ describe('NoteHeader component', () => {
username: 'root',
show_status: true,
status_tooltip_html: statusHtml,
- status: {
- availability: '',
- },
+ availability: '',
};
const createComponent = (props) => {
@@ -48,7 +47,7 @@ describe('NoteHeader component', () => {
actions,
}),
propsData: { ...props },
- stubs: { GlSprintf },
+ stubs: { GlSprintf, UserNameWithStatus },
});
};
@@ -110,7 +109,7 @@ describe('NoteHeader component', () => {
});
it('renders busy status if author availability is set', () => {
- createComponent({ author: { ...author, status: { availability: AVAILABILITY_STATUS.BUSY } } });
+ createComponent({ author: { ...author, availability: AVAILABILITY_STATUS.BUSY } });
expect(wrapper.find('.js-user-link').text()).toContain('(Busy)');
});
diff --git a/spec/frontend/notes/components/noteable_discussion_spec.js b/spec/frontend/notes/components/noteable_discussion_spec.js
index b87c6cd7f2b..87538279c3d 100644
--- a/spec/frontend/notes/components/noteable_discussion_spec.js
+++ b/spec/frontend/notes/components/noteable_discussion_spec.js
@@ -1,13 +1,13 @@
-import { nextTick } from 'vue';
import { mount } from '@vue/test-utils';
-import mockDiffFile from 'jest/diffs/mock_data/diff_file';
+import { nextTick } from 'vue';
import { trimText } from 'helpers/text_helper';
-import createStore from '~/notes/stores';
-import NoteableDiscussion from '~/notes/components/noteable_discussion.vue';
+import mockDiffFile from 'jest/diffs/mock_data/diff_file';
import DiscussionNotes from '~/notes/components/discussion_notes.vue';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
import ResolveWithIssueButton from '~/notes/components/discussion_resolve_with_issue_button.vue';
import NoteForm from '~/notes/components/note_form.vue';
+import NoteableDiscussion from '~/notes/components/noteable_discussion.vue';
+import createStore from '~/notes/stores';
import '~/behaviors/markdown/render_gfm';
import {
noteableDataMock,
diff --git a/spec/frontend/notes/components/noteable_note_spec.js b/spec/frontend/notes/components/noteable_note_spec.js
index 6f06665f412..fe78e086403 100644
--- a/spec/frontend/notes/components/noteable_note_spec.js
+++ b/spec/frontend/notes/components/noteable_note_spec.js
@@ -1,22 +1,13 @@
-import { escape } from 'lodash';
import { mount, createLocalVue } from '@vue/test-utils';
-import createStore from '~/notes/stores';
-import issueNote from '~/notes/components/noteable_note.vue';
-import NoteHeader from '~/notes/components/note_header.vue';
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import { escape } from 'lodash';
import NoteActions from '~/notes/components/note_actions.vue';
import NoteBody from '~/notes/components/note_body.vue';
+import NoteHeader from '~/notes/components/note_header.vue';
+import issueNote from '~/notes/components/noteable_note.vue';
+import createStore from '~/notes/stores';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import { noteableDataMock, notesDataMock, note } from '../mock_data';
-jest.mock('~/vue_shared/mixins/gl_feature_flags_mixin', () => () => ({
- inject: {
- glFeatures: {
- from: 'glFeatures',
- default: () => ({ multilineComments: true }),
- },
- },
-}));
-
describe('issue_note', () => {
let store;
let wrapper;
diff --git a/spec/frontend/notes/components/notes_app_spec.js b/spec/frontend/notes/components/notes_app_spec.js
index e495a4738e0..efee72dea96 100644
--- a/spec/frontend/notes/components/notes_app_spec.js
+++ b/spec/frontend/notes/components/notes_app_spec.js
@@ -1,18 +1,18 @@
-import $ from 'jquery';
+import { mount, shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
+import $ from 'jquery';
import Vue from 'vue';
-import { mount, shallowMount } from '@vue/test-utils';
import { setTestTimeout } from 'helpers/timeout';
import axios from '~/lib/utils/axios_utils';
-import NotesApp from '~/notes/components/notes_app.vue';
+import * as urlUtility from '~/lib/utils/url_utility';
import CommentForm from '~/notes/components/comment_form.vue';
-import createStore from '~/notes/stores';
+import NotesApp from '~/notes/components/notes_app.vue';
import * as constants from '~/notes/constants';
+import createStore from '~/notes/stores';
import '~/behaviors/markdown/render_gfm';
// TODO: use generated fixture (https://gitlab.com/gitlab-org/gitlab-foss/issues/62491)
-import * as mockData from '../mock_data';
-import * as urlUtility from '~/lib/utils/url_utility';
import OrderedLayout from '~/vue_shared/components/ordered_layout.vue';
+import * as mockData from '../mock_data';
jest.mock('~/user_popovers', () => jest.fn());
diff --git a/spec/frontend/notes/components/sort_discussion_spec.js b/spec/frontend/notes/components/sort_discussion_spec.js
index 739e247735d..60f03a0f5b5 100644
--- a/spec/frontend/notes/components/sort_discussion_spec.js
+++ b/spec/frontend/notes/components/sort_discussion_spec.js
@@ -1,10 +1,10 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import SortDiscussion from '~/notes/components/sort_discussion.vue';
-import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
-import createStore from '~/notes/stores';
import { ASC, DESC } from '~/notes/constants';
+import createStore from '~/notes/stores';
import Tracking from '~/tracking';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
diff --git a/spec/frontend/notes/components/timeline_toggle_spec.js b/spec/frontend/notes/components/timeline_toggle_spec.js
index b8df6fc7996..73fb2079e31 100644
--- a/spec/frontend/notes/components/timeline_toggle_spec.js
+++ b/spec/frontend/notes/components/timeline_toggle_spec.js
@@ -1,12 +1,12 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import TimelineToggle, {
timelineEnabledTooltip,
timelineDisabledTooltip,
} from '~/notes/components/timeline_toggle.vue';
-import createStore from '~/notes/stores';
import { ASC, DESC } from '~/notes/constants';
+import createStore from '~/notes/stores';
import { trackToggleTimelineView } from '~/notes/utils';
import Tracking from '~/tracking';
diff --git a/spec/frontend/notes/mixins/discussion_navigation_spec.js b/spec/frontend/notes/mixins/discussion_navigation_spec.js
index 9c9a648d213..6a6e47ffcc5 100644
--- a/spec/frontend/notes/mixins/discussion_navigation_spec.js
+++ b/spec/frontend/notes/mixins/discussion_navigation_spec.js
@@ -1,10 +1,10 @@
-import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
import { setHTMLFixture } from 'helpers/fixtures';
+import createEventHub from '~/helpers/event_hub_factory';
import * as utils from '~/lib/utils/common_utils';
-import discussionNavigation from '~/notes/mixins/discussion_navigation';
import eventHub from '~/notes/event_hub';
-import createEventHub from '~/helpers/event_hub_factory';
+import discussionNavigation from '~/notes/mixins/discussion_navigation';
import notesModule from '~/notes/stores/modules';
const discussion = (id, index) => ({
diff --git a/spec/frontend/notes/old_notes_spec.js b/spec/frontend/notes/old_notes_spec.js
index 00821980e8a..432b660c4b3 100644
--- a/spec/frontend/notes/old_notes_spec.js
+++ b/spec/frontend/notes/old_notes_spec.js
@@ -1,13 +1,13 @@
/* eslint-disable import/no-commonjs, no-new */
-import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter';
+import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
import { createSpyObj } from 'helpers/jest_helpers';
-import { setTestTimeoutOnce } from 'helpers/timeout';
import { TEST_HOST } from 'helpers/test_constants';
-import * as urlUtility from '~/lib/utils/url_utility';
+import { setTestTimeoutOnce } from 'helpers/timeout';
import axios from '~/lib/utils/axios_utils';
+import * as urlUtility from '~/lib/utils/url_utility';
// These must be imported synchronously because they pull dependencies
// from the DOM.
diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js
index f0e6a0a68dd..1852108b39f 100644
--- a/spec/frontend/notes/stores/actions_spec.js
+++ b/spec/frontend/notes/stores/actions_spec.js
@@ -1,13 +1,18 @@
-import { TEST_HOST } from 'spec/test_constants';
import AxiosMockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
+import { TEST_HOST } from 'spec/test_constants';
import Api from '~/api';
import { deprecatedCreateFlash as Flash } from '~/flash';
-import * as actions from '~/notes/stores/actions';
-import mutations from '~/notes/stores/mutations';
-import * as mutationTypes from '~/notes/stores/mutation_types';
+import axios from '~/lib/utils/axios_utils';
import * as notesConstants from '~/notes/constants';
import createStore from '~/notes/stores';
+import * as actions from '~/notes/stores/actions';
+import * as mutationTypes from '~/notes/stores/mutation_types';
+import mutations from '~/notes/stores/mutations';
+import * as utils from '~/notes/stores/utils';
+import updateIssueConfidentialMutation from '~/sidebar/components/confidential/mutations/update_issue_confidential.mutation.graphql';
+import updateIssueLockMutation from '~/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql';
+import updateMergeRequestLockMutation from '~/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql';
import mrWidgetEventHub from '~/vue_merge_request_widget/event_hub';
import { resetStore } from '../helpers';
import {
@@ -18,11 +23,6 @@ import {
individualNote,
batchSuggestionsInfoMock,
} from '../mock_data';
-import axios from '~/lib/utils/axios_utils';
-import * as utils from '~/notes/stores/utils';
-import updateIssueConfidentialMutation from '~/sidebar/components/confidential/mutations/update_issue_confidential.mutation.graphql';
-import updateMergeRequestLockMutation from '~/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql';
-import updateIssueLockMutation from '~/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql';
const TEST_ERROR_MESSAGE = 'Test error message';
jest.mock('~/flash');
@@ -291,9 +291,45 @@ describe('Actions Notes Store', () => {
[
{ type: 'updateOrCreateNotes', payload: discussionMock.notes },
{ type: 'startTaskList' },
+ { type: 'updateResolvableDiscussionsCounts' },
],
));
});
+
+ describe('paginated notes feature flag enabled', () => {
+ const lastFetchedAt = '12358';
+
+ beforeEach(() => {
+ window.gon = { features: { paginatedNotes: true } };
+
+ axiosMock.onGet(notesDataMock.notesPath).replyOnce(200, {
+ notes: discussionMock.notes,
+ more: false,
+ last_fetched_at: lastFetchedAt,
+ });
+ });
+
+ afterEach(() => {
+ window.gon = null;
+ });
+
+ it('should dispatch setFetchingState, setNotesFetchedState, setLoadingState, updateOrCreateNotes, startTaskList and commit SET_LAST_FETCHED_AT', () => {
+ return testAction(
+ actions.fetchData,
+ null,
+ { notesData: notesDataMock, isFetching: true },
+ [{ type: 'SET_LAST_FETCHED_AT', payload: lastFetchedAt }],
+ [
+ { type: 'setFetchingState', payload: false },
+ { type: 'setNotesFetchedState', payload: true },
+ { type: 'setLoadingState', payload: false },
+ { type: 'updateOrCreateNotes', payload: discussionMock.notes },
+ { type: 'startTaskList' },
+ { type: 'updateResolvableDiscussionsCounts' },
+ ],
+ );
+ });
+ });
});
describe('poll', () => {
@@ -1276,6 +1312,7 @@ describe('Actions Notes Store', () => {
return actions
.updateConfidentialityOnIssuable({ commit: commitSpy, state, getters }, actionArgs)
.then(() => {
+ expect(Flash).not.toHaveBeenCalled();
expect(commitSpy).toHaveBeenCalledWith(
mutationTypes.SET_ISSUE_CONFIDENTIAL,
confidential,
@@ -1283,6 +1320,22 @@ describe('Actions Notes Store', () => {
});
});
});
+
+ describe('on user recoverable error', () => {
+ it('sends the error to Flash', () => {
+ const error = 'error';
+
+ jest
+ .spyOn(utils.gqClient, 'mutate')
+ .mockResolvedValue({ data: { issueSetConfidential: { errors: [error] } } });
+
+ return actions
+ .updateConfidentialityOnIssuable({ commit: () => {}, state, getters }, actionArgs)
+ .then(() => {
+ expect(Flash).toHaveBeenCalledWith(error, 'alert');
+ });
+ });
+ });
});
describe.each`
@@ -1355,4 +1408,17 @@ describe('Actions Notes Store', () => {
);
});
});
+
+ describe('setFetchingState', () => {
+ it('commits SET_NOTES_FETCHING_STATE', (done) => {
+ testAction(
+ actions.setFetchingState,
+ true,
+ null,
+ [{ type: mutationTypes.SET_NOTES_FETCHING_STATE, payload: true }],
+ [],
+ done,
+ );
+ });
+ });
});
diff --git a/spec/frontend/notes/stores/getters_spec.js b/spec/frontend/notes/stores/getters_spec.js
index fd04d08b6a5..4ebfc679310 100644
--- a/spec/frontend/notes/stores/getters_spec.js
+++ b/spec/frontend/notes/stores/getters_spec.js
@@ -1,5 +1,5 @@
-import * as getters from '~/notes/stores/getters';
import { DESC } from '~/notes/constants';
+import * as getters from '~/notes/stores/getters';
import {
notesDataMock,
userDataMock,
diff --git a/spec/frontend/notes/stores/mutation_spec.js b/spec/frontend/notes/stores/mutation_spec.js
index 66fc74525ad..99e24f724f4 100644
--- a/spec/frontend/notes/stores/mutation_spec.js
+++ b/spec/frontend/notes/stores/mutation_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import mutations from '~/notes/stores/mutations';
import { DISCUSSION_NOTE, ASC, DESC } from '~/notes/constants';
+import mutations from '~/notes/stores/mutations';
import {
note,
discussionMock,
@@ -400,6 +400,19 @@ describe('Notes Store mutations', () => {
expect(state.discussions[0].notes[0].note).toEqual('Foo');
});
+ it('does not update existing note if it matches', () => {
+ const state = {
+ discussions: [{ ...individualNote, individual_note: false }],
+ };
+ jest.spyOn(state.discussions[0].notes, 'splice');
+
+ const updated = individualNote.notes[0];
+
+ mutations.UPDATE_NOTE(state, updated);
+
+ expect(state.discussions[0].notes.splice).not.toHaveBeenCalled();
+ });
+
it('transforms an individual note to discussion', () => {
const state = {
discussions: [individualNote],
diff --git a/spec/frontend/notifications/components/custom_notifications_modal_spec.js b/spec/frontend/notifications/components/custom_notifications_modal_spec.js
new file mode 100644
index 00000000000..3e87f3107bd
--- /dev/null
+++ b/spec/frontend/notifications/components/custom_notifications_modal_spec.js
@@ -0,0 +1,267 @@
+import { GlSprintf, GlModal, GlFormGroup, GlFormCheckbox, GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import httpStatus from '~/lib/utils/http_status';
+import CustomNotificationsModal from '~/notifications/components/custom_notifications_modal.vue';
+import { i18n } from '~/notifications/constants';
+
+const mockNotificationSettingsResponses = {
+ default: {
+ level: 'custom',
+ events: {
+ new_release: true,
+ new_note: false,
+ },
+ },
+ updated: {
+ level: 'custom',
+ events: {
+ new_release: true,
+ new_note: true,
+ },
+ },
+};
+
+const mockToastShow = jest.fn();
+
+describe('CustomNotificationsModal', () => {
+ let wrapper;
+ let mockAxios;
+
+ function createComponent(options = {}) {
+ const { injectedProperties = {}, props = {} } = options;
+ return extendedWrapper(
+ shallowMount(CustomNotificationsModal, {
+ props: {
+ ...props,
+ },
+ provide: {
+ ...injectedProperties,
+ },
+ mocks: {
+ $toast: {
+ show: mockToastShow,
+ },
+ },
+ stubs: {
+ GlModal,
+ GlFormGroup,
+ GlFormCheckbox,
+ },
+ }),
+ );
+ }
+
+ const findModalBodyDescription = () => wrapper.find(GlSprintf);
+ const findAllCheckboxes = () => wrapper.findAll(GlFormCheckbox);
+ const findCheckboxAt = (index) => findAllCheckboxes().at(index);
+
+ beforeEach(() => {
+ gon.api_version = 'v4';
+ mockAxios = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ mockAxios.restore();
+ });
+
+ describe('template', () => {
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ it('displays the body title and the body message', () => {
+ expect(wrapper.findByTestId('modalBodyTitle').text()).toBe(
+ i18n.customNotificationsModal.bodyTitle,
+ );
+ expect(findModalBodyDescription().attributes('message')).toContain(
+ i18n.customNotificationsModal.bodyMessage,
+ );
+ });
+
+ describe('checkbox items', () => {
+ beforeEach(async () => {
+ wrapper = createComponent();
+
+ wrapper.setData({
+ events: [
+ { id: 'new_release', enabled: true, name: 'New release', loading: false },
+ { id: 'new_note', enabled: false, name: 'New note', loading: true },
+ ],
+ });
+
+ await wrapper.vm.$nextTick();
+ });
+
+ it.each`
+ index | eventId | eventName | enabled | loading
+ ${0} | ${'new_release'} | ${'New release'} | ${true} | ${false}
+ ${1} | ${'new_note'} | ${'New note'} | ${false} | ${true}
+ `(
+ 'renders a checkbox for "$eventName" with checked=$enabled',
+ async ({ index, eventName, enabled, loading }) => {
+ const checkbox = findCheckboxAt(index);
+ expect(checkbox.text()).toContain(eventName);
+ expect(checkbox.vm.$attrs.checked).toBe(enabled);
+ expect(checkbox.find(GlLoadingIcon).exists()).toBe(loading);
+ },
+ );
+ });
+ });
+
+ describe('API calls', () => {
+ describe('load notification settings', () => {
+ beforeEach(() => {
+ jest.spyOn(axios, 'get');
+ });
+
+ it.each`
+ projectId | groupId | endpointUrl | notificationType | condition
+ ${1} | ${null} | ${'/api/v4/projects/1/notification_settings'} | ${'project'} | ${'a projectId is given'}
+ ${null} | ${1} | ${'/api/v4/groups/1/notification_settings'} | ${'group'} | ${'a groupId is given'}
+ ${null} | ${null} | ${'/api/v4/notification_settings'} | ${'global'} | ${'neither projectId nor groupId are given'}
+ `(
+ 'requests $notificationType notification settings when $condition',
+ async ({ projectId, groupId, endpointUrl }) => {
+ const injectedProperties = {
+ projectId,
+ groupId,
+ };
+
+ mockAxios
+ .onGet(endpointUrl)
+ .reply(httpStatus.OK, mockNotificationSettingsResponses.default);
+
+ wrapper = createComponent({ injectedProperties });
+
+ wrapper.find(GlModal).vm.$emit('show');
+
+ await waitForPromises();
+
+ expect(axios.get).toHaveBeenCalledWith(endpointUrl);
+ },
+ );
+
+ it('updates the loading state and the events property', async () => {
+ const endpointUrl = '/api/v4/notification_settings';
+
+ mockAxios
+ .onGet(endpointUrl)
+ .reply(httpStatus.OK, mockNotificationSettingsResponses.default);
+
+ wrapper = createComponent();
+
+ wrapper.find(GlModal).vm.$emit('show');
+ expect(wrapper.vm.isLoading).toBe(true);
+
+ await waitForPromises();
+
+ expect(axios.get).toHaveBeenCalledWith(endpointUrl);
+ expect(wrapper.vm.isLoading).toBe(false);
+ expect(wrapper.vm.events).toEqual([
+ { id: 'new_release', enabled: true, name: 'New release', loading: false },
+ { id: 'new_note', enabled: false, name: 'New note', loading: false },
+ ]);
+ });
+
+ it('shows a toast message when the request fails', async () => {
+ mockAxios.onGet('/api/v4/notification_settings').reply(httpStatus.NOT_FOUND, {});
+ wrapper = createComponent();
+
+ wrapper.find(GlModal).vm.$emit('show');
+
+ await waitForPromises();
+
+ expect(
+ mockToastShow,
+ ).toHaveBeenCalledWith(
+ 'An error occured while loading the notification settings. Please try again.',
+ { type: 'error' },
+ );
+ });
+ });
+
+ describe('update notification settings', () => {
+ beforeEach(() => {
+ jest.spyOn(axios, 'put');
+ });
+
+ it.each`
+ projectId | groupId | endpointUrl | notificationType | condition
+ ${1} | ${null} | ${'/api/v4/projects/1/notification_settings'} | ${'project'} | ${'a projectId is given'}
+ ${null} | ${1} | ${'/api/v4/groups/1/notification_settings'} | ${'group'} | ${'a groupId is given'}
+ ${null} | ${null} | ${'/api/v4/notification_settings'} | ${'global'} | ${'neither projectId nor groupId are given'}
+ `(
+ 'updates the $notificationType notification settings when $condition and the user clicks the checkbox ',
+ async ({ projectId, groupId, endpointUrl }) => {
+ mockAxios
+ .onGet(endpointUrl)
+ .reply(httpStatus.OK, mockNotificationSettingsResponses.default);
+
+ mockAxios
+ .onPut(endpointUrl)
+ .reply(httpStatus.OK, mockNotificationSettingsResponses.updated);
+
+ const injectedProperties = {
+ projectId,
+ groupId,
+ };
+
+ wrapper = createComponent({ injectedProperties });
+
+ wrapper.setData({
+ events: [
+ { id: 'new_release', enabled: true, name: 'New release', loading: false },
+ { id: 'new_note', enabled: false, name: 'New note', loading: false },
+ ],
+ });
+
+ await wrapper.vm.$nextTick();
+
+ findCheckboxAt(1).vm.$emit('change', true);
+
+ await waitForPromises();
+
+ expect(axios.put).toHaveBeenCalledWith(endpointUrl, {
+ new_note: true,
+ });
+
+ expect(wrapper.vm.events).toEqual([
+ { id: 'new_release', enabled: true, name: 'New release', loading: false },
+ { id: 'new_note', enabled: true, name: 'New note', loading: false },
+ ]);
+ },
+ );
+
+ it('shows a toast message when the request fails', async () => {
+ mockAxios.onPut('/api/v4/notification_settings').reply(httpStatus.NOT_FOUND, {});
+ wrapper = createComponent();
+
+ wrapper.setData({
+ events: [
+ { id: 'new_release', enabled: true, name: 'New release', loading: false },
+ { id: 'new_note', enabled: false, name: 'New note', loading: false },
+ ],
+ });
+
+ await wrapper.vm.$nextTick();
+
+ findCheckboxAt(1).vm.$emit('change', true);
+
+ await waitForPromises();
+
+ expect(
+ mockToastShow,
+ ).toHaveBeenCalledWith(
+ 'An error occured while updating the notification settings. Please try again.',
+ { type: 'error' },
+ );
+ });
+ });
+ });
+});
diff --git a/spec/frontend/notifications/components/notifications_dropdown_spec.js b/spec/frontend/notifications/components/notifications_dropdown_spec.js
new file mode 100644
index 00000000000..0673fb51a91
--- /dev/null
+++ b/spec/frontend/notifications/components/notifications_dropdown_spec.js
@@ -0,0 +1,274 @@
+import { GlButtonGroup, GlButton, GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import waitForPromises from 'helpers/wait_for_promises';
+import httpStatus from '~/lib/utils/http_status';
+import CustomNotificationsModal from '~/notifications/components/custom_notifications_modal.vue';
+import NotificationsDropdown from '~/notifications/components/notifications_dropdown.vue';
+import NotificationsDropdownItem from '~/notifications/components/notifications_dropdown_item.vue';
+
+const mockDropdownItems = ['global', 'watch', 'participating', 'mention', 'disabled'];
+const mockToastShow = jest.fn();
+
+describe('NotificationsDropdown', () => {
+ let wrapper;
+ let mockAxios;
+ let glModalDirective;
+
+ function createComponent(injectedProperties = {}) {
+ glModalDirective = jest.fn();
+
+ return shallowMount(NotificationsDropdown, {
+ stubs: {
+ GlButtonGroup,
+ GlDropdown,
+ GlDropdownItem,
+ NotificationsDropdownItem,
+ CustomNotificationsModal,
+ },
+ directives: {
+ GlTooltip: createMockDirective(),
+ glModal: {
+ bind(_, { value }) {
+ glModalDirective(value);
+ },
+ },
+ },
+ provide: {
+ dropdownItems: mockDropdownItems,
+ initialNotificationLevel: 'global',
+ ...injectedProperties,
+ },
+ mocks: {
+ $toast: {
+ show: mockToastShow,
+ },
+ },
+ });
+ }
+
+ const findButtonGroup = () => wrapper.find(GlButtonGroup);
+ const findButton = () => wrapper.find(GlButton);
+ const findDropdown = () => wrapper.find(GlDropdown);
+ const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`);
+ const findAllNotificationsDropdownItems = () => wrapper.findAll(NotificationsDropdownItem);
+ const findDropdownItemAt = (index) =>
+ findAllNotificationsDropdownItems().at(index).find(GlDropdownItem);
+
+ const clickDropdownItemAt = async (index) => {
+ const dropdownItem = findDropdownItemAt(index);
+ dropdownItem.vm.$emit('click');
+
+ await waitForPromises();
+ };
+
+ beforeEach(() => {
+ gon.api_version = 'v4';
+ mockAxios = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ mockAxios.restore();
+ });
+
+ describe('template', () => {
+ describe('when notification level is "custom"', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ initialNotificationLevel: 'custom',
+ });
+ });
+
+ it('renders a button group', () => {
+ expect(findButtonGroup().exists()).toBe(true);
+ });
+
+ it('shows the button text when showLabel is true', () => {
+ wrapper = createComponent({
+ initialNotificationLevel: 'custom',
+ showLabel: true,
+ });
+
+ expect(findButton().text()).toBe('Custom');
+ });
+
+ it("doesn't show the button text when showLabel is false", () => {
+ wrapper = createComponent({
+ initialNotificationLevel: 'custom',
+ showLabel: false,
+ });
+
+ expect(findButton().text()).toBe('');
+ });
+
+ it('opens the modal when the user clicks the button', async () => {
+ jest.spyOn(axios, 'put');
+ mockAxios.onPut('/api/v4/notification_settings').reply(httpStatus.OK, {});
+
+ wrapper = createComponent({
+ initialNotificationLevel: 'custom',
+ });
+
+ findButton().vm.$emit('click');
+
+ expect(glModalDirective).toHaveBeenCalled();
+ });
+ });
+
+ describe('when notification level is not "custom"', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ initialNotificationLevel: 'global',
+ });
+ });
+
+ it('does not render a button group', () => {
+ expect(findButtonGroup().exists()).toBe(false);
+ });
+
+ it('shows the button text when showLabel is true', () => {
+ wrapper = createComponent({
+ showLabel: true,
+ });
+
+ expect(findDropdown().props('text')).toBe('Global');
+ });
+
+ it("doesn't show the button text when showLabel is false", () => {
+ wrapper = createComponent({
+ showLabel: false,
+ });
+
+ expect(findDropdown().props('text')).toBe(null);
+ });
+ });
+
+ describe('button tooltip', () => {
+ const tooltipTitlePrefix = 'Notification setting';
+ it.each`
+ level | title
+ ${'global'} | ${'Global'}
+ ${'watch'} | ${'Watch'}
+ ${'participating'} | ${'Participate'}
+ ${'mention'} | ${'On mention'}
+ ${'disabled'} | ${'Disabled'}
+ ${'custom'} | ${'Custom'}
+ `(`renders "${tooltipTitlePrefix} - $title" for "$level" level`, ({ level, title }) => {
+ wrapper = createComponent({
+ initialNotificationLevel: level,
+ });
+
+ const tooltipElement = findByTestId('notificationButton');
+ const tooltip = getBinding(tooltipElement.element, 'gl-tooltip');
+
+ expect(tooltip.value.title).toBe(`${tooltipTitlePrefix} - ${title}`);
+ });
+ });
+
+ describe('button icon', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ initialNotificationLevel: 'disabled',
+ });
+ });
+
+ it('renders the "notifications-off" icon when notification level is "disabled"', () => {
+ expect(findDropdown().props('icon')).toBe('notifications-off');
+ });
+
+ it('renders the "notifications" icon when notification level is not "disabled"', () => {
+ wrapper = createComponent();
+
+ expect(findDropdown().props('icon')).toBe('notifications');
+ });
+ });
+
+ describe('dropdown items', () => {
+ it.each`
+ dropdownIndex | level | title | description
+ ${0} | ${'global'} | ${'Global'} | ${'Use your global notification setting'}
+ ${1} | ${'watch'} | ${'Watch'} | ${'You will receive notifications for any activity'}
+ ${2} | ${'participating'} | ${'Participate'} | ${'You will only receive notifications for threads you have participated in'}
+ ${3} | ${'mention'} | ${'On mention'} | ${'You will receive notifications only for comments in which you were @mentioned'}
+ ${4} | ${'disabled'} | ${'Disabled'} | ${'You will not get any notifications via email'}
+ ${5} | ${'custom'} | ${'Custom'} | ${'You will only receive notifications for the events you choose'}
+ `('displays "$title" and "$description"', ({ dropdownIndex, title, description }) => {
+ wrapper = createComponent();
+
+ expect(findAllNotificationsDropdownItems().at(dropdownIndex).props('title')).toBe(title);
+ expect(findAllNotificationsDropdownItems().at(dropdownIndex).props('description')).toBe(
+ description,
+ );
+ });
+ });
+ });
+
+ describe('when selecting an item', () => {
+ beforeEach(() => {
+ jest.spyOn(axios, 'put');
+ });
+
+ it.each`
+ projectId | groupId | endpointUrl | endpointType | condition
+ ${1} | ${null} | ${'/api/v4/projects/1/notification_settings'} | ${'project notifications'} | ${'a projectId is given'}
+ ${null} | ${1} | ${'/api/v4/groups/1/notification_settings'} | ${'group notifications'} | ${'a groupId is given'}
+ ${null} | ${null} | ${'/api/v4/notification_settings'} | ${'global notifications'} | ${'when neither projectId nor groupId are given'}
+ `(
+ 'calls the $endpointType endpoint when $condition',
+ async ({ projectId, groupId, endpointUrl }) => {
+ wrapper = createComponent({
+ projectId,
+ groupId,
+ });
+
+ await clickDropdownItemAt(1);
+
+ expect(axios.put).toHaveBeenCalledWith(endpointUrl, {
+ level: 'watch',
+ });
+ },
+ );
+
+ it('updates the selectedNotificationLevel and marks the item with a checkmark', async () => {
+ mockAxios.onPut('/api/v4/notification_settings').reply(httpStatus.OK, {});
+ wrapper = createComponent();
+
+ const dropdownItem = findDropdownItemAt(1);
+
+ await clickDropdownItemAt(1);
+
+ expect(wrapper.vm.selectedNotificationLevel).toBe('watch');
+ expect(dropdownItem.props('isChecked')).toBe(true);
+ });
+
+ it("won't update the selectedNotificationLevel and shows a toast message when the request fails and ", async () => {
+ mockAxios.onPut('/api/v4/notification_settings').reply(httpStatus.NOT_FOUND, {});
+ wrapper = createComponent();
+
+ await clickDropdownItemAt(1);
+
+ expect(wrapper.vm.selectedNotificationLevel).toBe('global');
+ expect(
+ mockToastShow,
+ ).toHaveBeenCalledWith(
+ 'An error occured while updating the notification settings. Please try again.',
+ { type: 'error' },
+ );
+ });
+
+ it('opens the modal when the user clicks on the "Custom" dropdown item', async () => {
+ mockAxios.onPut('/api/v4/notification_settings').reply(httpStatus.OK, {});
+ wrapper = createComponent();
+
+ const mockModalShow = jest.spyOn(wrapper.vm.$refs.customNotificationsModal, 'open');
+
+ await clickDropdownItemAt(5);
+
+ expect(mockModalShow).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/onboarding_issues/index_spec.js b/spec/frontend/onboarding_issues/index_spec.js
deleted file mode 100644
index d476ba1cf5a..00000000000
--- a/spec/frontend/onboarding_issues/index_spec.js
+++ /dev/null
@@ -1,137 +0,0 @@
-import $ from 'jquery';
-import setWindowLocation from 'helpers/set_window_location_helper';
-import { showLearnGitLabIssuesPopover } from '~/onboarding_issues';
-import { getCookie, setCookie, removeCookie } from '~/lib/utils/common_utils';
-import Tracking from '~/tracking';
-
-describe('Onboarding Issues Popovers', () => {
- const COOKIE_NAME = 'onboarding_issues_settings';
- const getCookieValue = () => JSON.parse(getCookie(COOKIE_NAME));
-
- beforeEach(() => {
- jest.spyOn($.fn, 'popover');
- });
-
- afterEach(() => {
- $.fn.popover.mockRestore();
- document.getElementsByTagName('html')[0].innerHTML = '';
- removeCookie(COOKIE_NAME);
- });
-
- const setupShowLearnGitLabIssuesPopoverTest = ({
- currentPath = 'group/learn-gitlab',
- isIssuesBoardsLinkShown = true,
- isCookieSet = true,
- cookieValue = true,
- } = {}) => {
- setWindowLocation(`http://example.com/${currentPath}`);
-
- if (isIssuesBoardsLinkShown) {
- const elem = document.createElement('a');
- elem.setAttribute('data-qa-selector', 'issue_boards_link');
- document.body.appendChild(elem);
- }
-
- if (isCookieSet) {
- setCookie(COOKIE_NAME, { previous: true, 'issues#index': cookieValue });
- }
-
- showLearnGitLabIssuesPopover();
- };
-
- describe('showLearnGitLabIssuesPopover', () => {
- describe('when on another project', () => {
- beforeEach(() => {
- setupShowLearnGitLabIssuesPopoverTest({
- currentPath: 'group/another-project',
- });
- });
-
- it('does not show a popover', () => {
- expect($.fn.popover).not.toHaveBeenCalled();
- });
- });
-
- describe('when the issues boards link is not shown', () => {
- beforeEach(() => {
- setupShowLearnGitLabIssuesPopoverTest({
- isIssuesBoardsLinkShown: false,
- });
- });
-
- it('does not show a popover', () => {
- expect($.fn.popover).not.toHaveBeenCalled();
- });
- });
-
- describe('when the cookie is not set', () => {
- beforeEach(() => {
- setupShowLearnGitLabIssuesPopoverTest({
- isCookieSet: false,
- });
- });
-
- it('does not show a popover', () => {
- expect($.fn.popover).not.toHaveBeenCalled();
- });
- });
-
- describe('when the cookie value is false', () => {
- beforeEach(() => {
- setupShowLearnGitLabIssuesPopoverTest({
- cookieValue: false,
- });
- });
-
- it('does not show a popover', () => {
- expect($.fn.popover).not.toHaveBeenCalled();
- });
- });
-
- describe('with all the right conditions', () => {
- beforeEach(() => {
- setupShowLearnGitLabIssuesPopoverTest();
- });
-
- it('shows a popover', () => {
- expect($.fn.popover).toHaveBeenCalled();
- });
-
- it('does not change the cookie value', () => {
- expect(getCookieValue()['issues#index']).toBe(true);
- });
-
- it('disables the previous popover', () => {
- expect(getCookieValue().previous).toBe(false);
- });
-
- describe('when clicking the issues boards link', () => {
- beforeEach(() => {
- document.querySelector('a[data-qa-selector="issue_boards_link"]').click();
- });
-
- it('deletes the cookie', () => {
- expect(getCookie(COOKIE_NAME)).toBe(undefined);
- });
- });
-
- describe('when dismissing the popover', () => {
- beforeEach(() => {
- jest.spyOn(Tracking, 'event');
- document.querySelector('.learn-gitlab.popover .close').click();
- });
-
- it('deletes the cookie', () => {
- expect(getCookie(COOKIE_NAME)).toBe(undefined);
- });
-
- it('sends a tracking event', () => {
- expect(Tracking.event).toHaveBeenCalledWith(
- 'Growth::Conversion::Experiment::OnboardingIssues',
- 'dismiss_popover',
- );
- });
- });
- });
- });
-});
diff --git a/spec/frontend/operation_settings/components/metrics_settings_spec.js b/spec/frontend/operation_settings/components/metrics_settings_spec.js
index 3216eece391..272e9b71f67 100644
--- a/spec/frontend/operation_settings/components/metrics_settings_spec.js
+++ b/spec/frontend/operation_settings/components/metrics_settings_spec.js
@@ -1,15 +1,15 @@
-import { mount, shallowMount } from '@vue/test-utils';
import { GlButton, GlLink, GlFormGroup, GlFormInput, GlFormSelect } from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import { refreshCurrentPage } from '~/lib/utils/url_utility';
+import { timezones } from '~/monitoring/format_date';
+import DashboardTimezone from '~/operation_settings/components/form_group/dashboard_timezone.vue';
+import ExternalDashboard from '~/operation_settings/components/form_group/external_dashboard.vue';
import MetricsSettings from '~/operation_settings/components/metrics_settings.vue';
-import ExternalDashboard from '~/operation_settings/components/form_group/external_dashboard.vue';
-import DashboardTimezone from '~/operation_settings/components/form_group/dashboard_timezone.vue';
-import { timezones } from '~/monitoring/format_date';
import store from '~/operation_settings/store';
-import axios from '~/lib/utils/axios_utils';
-import { refreshCurrentPage } from '~/lib/utils/url_utility';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
jest.mock('~/lib/utils/url_utility');
jest.mock('~/flash');
diff --git a/spec/frontend/operation_settings/store/mutations_spec.js b/spec/frontend/operation_settings/store/mutations_spec.js
index 88eb66095ad..db6b54b503d 100644
--- a/spec/frontend/operation_settings/store/mutations_spec.js
+++ b/spec/frontend/operation_settings/store/mutations_spec.js
@@ -1,6 +1,6 @@
+import { timezones } from '~/monitoring/format_date';
import mutations from '~/operation_settings/store/mutations';
import createState from '~/operation_settings/store/state';
-import { timezones } from '~/monitoring/format_date';
describe('operation settings mutations', () => {
let localState;
diff --git a/spec/frontend/packages/details/components/additional_metadata_spec.js b/spec/frontend/packages/details/components/additional_metadata_spec.js
index 8466a630ecb..b339aa84348 100644
--- a/spec/frontend/packages/details/components/additional_metadata_spec.js
+++ b/spec/frontend/packages/details/components/additional_metadata_spec.js
@@ -1,7 +1,7 @@
-import { shallowMount } from '@vue/test-utils';
import { GlLink, GlSprintf } from '@gitlab/ui';
-import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
+import { shallowMount } from '@vue/test-utils';
import component from '~/packages/details/components/additional_metadata.vue';
+import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
import { mavenPackage, conanPackage, nugetPackage, npmPackage } from '../../mock_data';
diff --git a/spec/frontend/packages/details/components/app_spec.js b/spec/frontend/packages/details/components/app_spec.js
index 97df117df0b..11dad7ba34d 100644
--- a/spec/frontend/packages/details/components/app_spec.js
+++ b/spec/frontend/packages/details/components/app_spec.js
@@ -1,22 +1,21 @@
-import Vuex from 'vuex';
-import { mount, createLocalVue } from '@vue/test-utils';
import { GlEmptyState, GlModal } from '@gitlab/ui';
+import { mount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
import stubChildren from 'helpers/stub_children';
-import Tracking from '~/tracking';
-import * as getters from '~/packages/details/store/getters';
-import PackagesApp from '~/packages/details/components/app.vue';
-import PackageTitle from '~/packages/details/components/package_title.vue';
-
-import * as SharedUtils from '~/packages/shared/utils';
-import { TrackingActions } from '~/packages/shared/constants';
-import PackagesListLoader from '~/packages/shared/components/packages_list_loader.vue';
-import PackageListRow from '~/packages/shared/components/package_list_row.vue';
-import DependencyRow from '~/packages/details/components/dependency_row.vue';
-import PackageHistory from '~/packages/details/components/package_history.vue';
import AdditionalMetadata from '~/packages/details/components/additional_metadata.vue';
+import PackagesApp from '~/packages/details/components/app.vue';
+import DependencyRow from '~/packages/details/components/dependency_row.vue';
import InstallationCommands from '~/packages/details/components/installation_commands.vue';
import PackageFiles from '~/packages/details/components/package_files.vue';
+import PackageHistory from '~/packages/details/components/package_history.vue';
+import PackageTitle from '~/packages/details/components/package_title.vue';
+import * as getters from '~/packages/details/store/getters';
+import PackageListRow from '~/packages/shared/components/package_list_row.vue';
+import PackagesListLoader from '~/packages/shared/components/packages_list_loader.vue';
+import { TrackingActions } from '~/packages/shared/constants';
+import * as SharedUtils from '~/packages/shared/utils';
+import Tracking from '~/tracking';
import {
composerPackage,
diff --git a/spec/frontend/packages/details/components/composer_installation_spec.js b/spec/frontend/packages/details/components/composer_installation_spec.js
index b44609e8ae7..a1d30d0ed22 100644
--- a/spec/frontend/packages/details/components/composer_installation_spec.js
+++ b/spec/frontend/packages/details/components/composer_installation_spec.js
@@ -1,6 +1,6 @@
-import Vuex from 'vuex';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlSprintf, GlLink } from '@gitlab/ui';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
import { registryUrl as composerHelpPath } from 'jest/packages/details/mock_data';
import { composerPackage as packageEntity } from 'jest/packages/mock_data';
import ComposerInstallation from '~/packages/details/components/composer_installation.vue';
diff --git a/spec/frontend/packages/details/components/conan_installation_spec.js b/spec/frontend/packages/details/components/conan_installation_spec.js
index 92c1f8e7f4a..bf8a92a6350 100644
--- a/spec/frontend/packages/details/components/conan_installation_spec.js
+++ b/spec/frontend/packages/details/components/conan_installation_spec.js
@@ -1,5 +1,5 @@
-import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
import ConanInstallation from '~/packages/details/components/conan_installation.vue';
import CodeInstructions from '~/vue_shared/components/registry/code_instruction.vue';
import { conanPackage as packageEntity } from '../../mock_data';
diff --git a/spec/frontend/packages/details/components/installations_commands_spec.js b/spec/frontend/packages/details/components/installations_commands_spec.js
index 60da34ebcd9..065bf503585 100644
--- a/spec/frontend/packages/details/components/installations_commands_spec.js
+++ b/spec/frontend/packages/details/components/installations_commands_spec.js
@@ -1,12 +1,12 @@
import { shallowMount } from '@vue/test-utils';
+import ComposerInstallation from '~/packages/details/components/composer_installation.vue';
+import ConanInstallation from '~/packages/details/components/conan_installation.vue';
import InstallationCommands from '~/packages/details/components/installation_commands.vue';
-import NpmInstallation from '~/packages/details/components/npm_installation.vue';
import MavenInstallation from '~/packages/details/components/maven_installation.vue';
-import ConanInstallation from '~/packages/details/components/conan_installation.vue';
+import NpmInstallation from '~/packages/details/components/npm_installation.vue';
import NugetInstallation from '~/packages/details/components/nuget_installation.vue';
import PypiInstallation from '~/packages/details/components/pypi_installation.vue';
-import ComposerInstallation from '~/packages/details/components/composer_installation.vue';
import {
conanPackage,
diff --git a/spec/frontend/packages/details/components/maven_installation_spec.js b/spec/frontend/packages/details/components/maven_installation_spec.js
index ff62969e709..dfeb6002186 100644
--- a/spec/frontend/packages/details/components/maven_installation_spec.js
+++ b/spec/frontend/packages/details/components/maven_installation_spec.js
@@ -1,10 +1,10 @@
-import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
import { registryUrl as mavenPath } from 'jest/packages/details/mock_data';
import { mavenPackage as packageEntity } from 'jest/packages/mock_data';
import MavenInstallation from '~/packages/details/components/maven_installation.vue';
-import CodeInstructions from '~/vue_shared/components/registry/code_instruction.vue';
import { TrackingActions } from '~/packages/details/constants';
+import CodeInstructions from '~/vue_shared/components/registry/code_instruction.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
diff --git a/spec/frontend/packages/details/components/npm_installation_spec.js b/spec/frontend/packages/details/components/npm_installation_spec.js
index dd6e62185a9..df820e7e948 100644
--- a/spec/frontend/packages/details/components/npm_installation_spec.js
+++ b/spec/frontend/packages/details/components/npm_installation_spec.js
@@ -1,11 +1,11 @@
-import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
-import { npmPackage as packageEntity } from 'jest/packages/mock_data';
+import Vuex from 'vuex';
import { registryUrl as nugetPath } from 'jest/packages/details/mock_data';
+import { npmPackage as packageEntity } from 'jest/packages/mock_data';
import NpmInstallation from '~/packages/details/components/npm_installation.vue';
-import CodeInstructions from '~/vue_shared/components/registry/code_instruction.vue';
import { TrackingActions } from '~/packages/details/constants';
import { npmInstallationCommand, npmSetupCommand } from '~/packages/details/store/getters';
+import CodeInstructions from '~/vue_shared/components/registry/code_instruction.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
diff --git a/spec/frontend/packages/details/components/nuget_installation_spec.js b/spec/frontend/packages/details/components/nuget_installation_spec.js
index 685d0808dd9..100e369751c 100644
--- a/spec/frontend/packages/details/components/nuget_installation_spec.js
+++ b/spec/frontend/packages/details/components/nuget_installation_spec.js
@@ -1,10 +1,10 @@
-import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
-import { nugetPackage as packageEntity } from 'jest/packages/mock_data';
+import Vuex from 'vuex';
import { registryUrl as nugetPath } from 'jest/packages/details/mock_data';
+import { nugetPackage as packageEntity } from 'jest/packages/mock_data';
import NugetInstallation from '~/packages/details/components/nuget_installation.vue';
-import CodeInstructions from '~/vue_shared/components/registry/code_instruction.vue';
import { TrackingActions } from '~/packages/details/constants';
+import CodeInstructions from '~/vue_shared/components/registry/code_instruction.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
diff --git a/spec/frontend/packages/details/components/package_files_spec.js b/spec/frontend/packages/details/components/package_files_spec.js
index 9bcf6ed9235..b4e62bac8a3 100644
--- a/spec/frontend/packages/details/components/package_files_spec.js
+++ b/spec/frontend/packages/details/components/package_files_spec.js
@@ -1,8 +1,8 @@
import { mount } from '@vue/test-utils';
import stubChildren from 'helpers/stub_children';
-import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import FileIcon from '~/vue_shared/components/file_icon.vue';
import component from '~/packages/details/components/package_files.vue';
+import FileIcon from '~/vue_shared/components/file_icon.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { npmFiles, mavenFiles } from '../../mock_data';
diff --git a/spec/frontend/packages/details/components/package_history_spec.js b/spec/frontend/packages/details/components/package_history_spec.js
index 5a6b386e2ca..244805a9c82 100644
--- a/spec/frontend/packages/details/components/package_history_spec.js
+++ b/spec/frontend/packages/details/components/package_history_spec.js
@@ -1,10 +1,10 @@
-import { shallowMount } from '@vue/test-utils';
import { GlLink, GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { stubComponent } from 'helpers/stub_component';
-import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import HistoryItem from '~/vue_shared/components/registry/history_item.vue';
-import { HISTORY_PIPELINES_LIMIT } from '~/packages/details/constants';
import component from '~/packages/details/components/package_history.vue';
+import { HISTORY_PIPELINES_LIMIT } from '~/packages/details/constants';
+import HistoryItem from '~/vue_shared/components/registry/history_item.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { mavenPackage, mockPipelineInfo } from '../../mock_data';
diff --git a/spec/frontend/packages/details/components/package_title_spec.js b/spec/frontend/packages/details/components/package_title_spec.js
index 61c6e824ab7..512cec85b40 100644
--- a/spec/frontend/packages/details/components/package_title_spec.js
+++ b/spec/frontend/packages/details/components/package_title_spec.js
@@ -1,6 +1,6 @@
-import Vuex from 'vuex';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
import PackageTitle from '~/packages/details/components/package_title.vue';
import PackageTags from '~/packages/shared/components/package_tags.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
diff --git a/spec/frontend/packages/details/components/pypi_installation_spec.js b/spec/frontend/packages/details/components/pypi_installation_spec.js
index da30b4ba565..a6ccba71554 100644
--- a/spec/frontend/packages/details/components/pypi_installation_spec.js
+++ b/spec/frontend/packages/details/components/pypi_installation_spec.js
@@ -1,5 +1,5 @@
-import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
import { pypiPackage as packageEntity } from 'jest/packages/mock_data';
import PypiInstallation from '~/packages/details/components/pypi_installation.vue';
diff --git a/spec/frontend/packages/details/store/actions_spec.js b/spec/frontend/packages/details/store/actions_spec.js
index e823a00ebc4..d11ee548b72 100644
--- a/spec/frontend/packages/details/store/actions_spec.js
+++ b/spec/frontend/packages/details/store/actions_spec.js
@@ -1,9 +1,9 @@
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import { FETCH_PACKAGE_VERSIONS_ERROR } from '~/packages/details/constants';
import { fetchPackageVersions, deletePackage } from '~/packages/details/store/actions';
import * as types from '~/packages/details/store/mutation_types';
-import { FETCH_PACKAGE_VERSIONS_ERROR } from '~/packages/details/constants';
import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages/shared/constants';
import { npmPackage as packageEntity } from '../../mock_data';
diff --git a/spec/frontend/packages/details/store/getters_spec.js b/spec/frontend/packages/details/store/getters_spec.js
index b8c2138e7f5..07c120f57f7 100644
--- a/spec/frontend/packages/details/store/getters_spec.js
+++ b/spec/frontend/packages/details/store/getters_spec.js
@@ -1,3 +1,4 @@
+import { NpmManager } from '~/packages/details/constants';
import {
conanInstallationCommand,
conanSetupCommand,
@@ -32,7 +33,6 @@ import {
registryUrl,
pypiSetupCommandStr,
} from '../mock_data';
-import { NpmManager } from '~/packages/details/constants';
describe('Getters PackageDetails Store', () => {
let state;
diff --git a/spec/frontend/packages/details/store/mutations_spec.js b/spec/frontend/packages/details/store/mutations_spec.js
index 501a56dcdde..6bc5fb7241f 100644
--- a/spec/frontend/packages/details/store/mutations_spec.js
+++ b/spec/frontend/packages/details/store/mutations_spec.js
@@ -1,5 +1,5 @@
-import mutations from '~/packages/details/store/mutations';
import * as types from '~/packages/details/store/mutation_types';
+import mutations from '~/packages/details/store/mutations';
import { npmPackage as packageEntity } from '../../mock_data';
describe('Mutations package details Store', () => {
diff --git a/spec/frontend/packages/list/components/__snapshots__/packages_filter_spec.js.snap b/spec/frontend/packages/list/components/__snapshots__/packages_filter_spec.js.snap
deleted file mode 100644
index ed77f25916f..00000000000
--- a/spec/frontend/packages/list/components/__snapshots__/packages_filter_spec.js.snap
+++ /dev/null
@@ -1,14 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`packages_filter renders 1`] = `
-<gl-search-box-by-click-stub
- clearable="true"
- clearbuttontitle="Clear"
- clearrecentsearchestext="Clear recent searches"
- closebuttontitle="Close"
- norecentsearchestext="You don't have any recent searches"
- placeholder="Filter by name"
- recentsearchesheader="Recent searches"
- value=""
-/>
-`;
diff --git a/spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap b/spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap
index b2df1ac5ab6..3f17731584c 100644
--- a/spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap
+++ b/spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap
@@ -6,517 +6,60 @@ exports[`packages_list_app renders 1`] = `
packagehelpurl="foo"
/>
- <b-tabs-stub
- activenavitemclass="gl-tab-nav-item-active gl-tab-nav-item-active-indigo"
- class="gl-tabs"
- contentclass=",gl-tab-content"
- navclass=",gl-tabs-nav"
- nofade="true"
- nonavstyle="true"
- tag="div"
- >
- <template>
-
- <b-tab-stub
- tag="div"
- title="All"
- titlelinkclass="gl-tab-nav-item"
- >
- <template>
- <div>
- <section
- class="row empty-state text-center"
- >
- <div
- class="col-12"
- >
- <div
- class="svg-250 svg-content"
- >
- <img
- alt=""
- class="gl-max-w-full"
- src="helpSvg"
- />
- </div>
- </div>
-
- <div
- class="col-12"
- >
- <div
- class="text-content gl-mx-auto gl-my-0 gl-p-5"
- >
- <h1
- class="h4"
- >
- There are no packages yet
- </h1>
-
- <p>
- Learn how to
- <b-link-stub
- class="gl-link"
- event="click"
- href="helpUrl"
- routertag="a"
- target="_blank"
- >
- publish and share your packages
- </b-link-stub>
- with GitLab.
- </p>
-
- <div>
- <!---->
-
- <!---->
- </div>
- </div>
- </div>
- </section>
- </div>
- </template>
- </b-tab-stub>
- <b-tab-stub
- tag="div"
- title="Composer"
- titlelinkclass="gl-tab-nav-item"
- >
- <template>
- <div>
- <section
- class="row empty-state text-center"
- >
- <div
- class="col-12"
- >
- <div
- class="svg-250 svg-content"
- >
- <img
- alt=""
- class="gl-max-w-full"
- src="helpSvg"
- />
- </div>
- </div>
-
- <div
- class="col-12"
- >
- <div
- class="text-content gl-mx-auto gl-my-0 gl-p-5"
- >
- <h1
- class="h4"
- >
- There are no Composer packages yet
- </h1>
-
- <p>
- Learn how to
- <b-link-stub
- class="gl-link"
- event="click"
- href="helpUrl"
- routertag="a"
- target="_blank"
- >
- publish and share your packages
- </b-link-stub>
- with GitLab.
- </p>
-
- <div>
- <!---->
-
- <!---->
- </div>
- </div>
- </div>
- </section>
- </div>
- </template>
- </b-tab-stub>
- <b-tab-stub
- tag="div"
- title="Conan"
- titlelinkclass="gl-tab-nav-item"
- >
- <template>
- <div>
- <section
- class="row empty-state text-center"
- >
- <div
- class="col-12"
- >
- <div
- class="svg-250 svg-content"
- >
- <img
- alt=""
- class="gl-max-w-full"
- src="helpSvg"
- />
- </div>
- </div>
-
- <div
- class="col-12"
- >
- <div
- class="text-content gl-mx-auto gl-my-0 gl-p-5"
- >
- <h1
- class="h4"
- >
- There are no Conan packages yet
- </h1>
-
- <p>
- Learn how to
- <b-link-stub
- class="gl-link"
- event="click"
- href="helpUrl"
- routertag="a"
- target="_blank"
- >
- publish and share your packages
- </b-link-stub>
- with GitLab.
- </p>
-
- <div>
- <!---->
-
- <!---->
- </div>
- </div>
- </div>
- </section>
- </div>
- </template>
- </b-tab-stub>
- <b-tab-stub
- tag="div"
- title="Generic"
- titlelinkclass="gl-tab-nav-item"
- >
- <template>
- <div>
- <section
- class="row empty-state text-center"
- >
- <div
- class="col-12"
- >
- <div
- class="svg-250 svg-content"
- >
- <img
- alt=""
- class="gl-max-w-full"
- src="helpSvg"
- />
- </div>
- </div>
-
- <div
- class="col-12"
- >
- <div
- class="text-content gl-mx-auto gl-my-0 gl-p-5"
- >
- <h1
- class="h4"
- >
- There are no Generic packages yet
- </h1>
-
- <p>
- Learn how to
- <b-link-stub
- class="gl-link"
- event="click"
- href="helpUrl"
- routertag="a"
- target="_blank"
- >
- publish and share your packages
- </b-link-stub>
- with GitLab.
- </p>
-
- <div>
- <!---->
-
- <!---->
- </div>
- </div>
- </div>
- </section>
- </div>
- </template>
- </b-tab-stub>
- <b-tab-stub
- tag="div"
- title="Maven"
- titlelinkclass="gl-tab-nav-item"
- >
- <template>
- <div>
- <section
- class="row empty-state text-center"
- >
- <div
- class="col-12"
- >
- <div
- class="svg-250 svg-content"
- >
- <img
- alt=""
- class="gl-max-w-full"
- src="helpSvg"
- />
- </div>
- </div>
-
- <div
- class="col-12"
- >
- <div
- class="text-content gl-mx-auto gl-my-0 gl-p-5"
- >
- <h1
- class="h4"
- >
- There are no Maven packages yet
- </h1>
-
- <p>
- Learn how to
- <b-link-stub
- class="gl-link"
- event="click"
- href="helpUrl"
- routertag="a"
- target="_blank"
- >
- publish and share your packages
- </b-link-stub>
- with GitLab.
- </p>
-
- <div>
- <!---->
-
- <!---->
- </div>
- </div>
- </div>
- </section>
- </div>
- </template>
- </b-tab-stub>
- <b-tab-stub
- tag="div"
- title="NPM"
- titlelinkclass="gl-tab-nav-item"
+ <package-search-stub />
+
+ <div>
+ <section
+ class="row empty-state text-center"
+ >
+ <div
+ class="col-12"
>
- <template>
- <div>
- <section
- class="row empty-state text-center"
- >
- <div
- class="col-12"
- >
- <div
- class="svg-250 svg-content"
- >
- <img
- alt=""
- class="gl-max-w-full"
- src="helpSvg"
- />
- </div>
- </div>
-
- <div
- class="col-12"
- >
- <div
- class="text-content gl-mx-auto gl-my-0 gl-p-5"
- >
- <h1
- class="h4"
- >
- There are no NPM packages yet
- </h1>
-
- <p>
- Learn how to
- <b-link-stub
- class="gl-link"
- event="click"
- href="helpUrl"
- routertag="a"
- target="_blank"
- >
- publish and share your packages
- </b-link-stub>
- with GitLab.
- </p>
-
- <div>
- <!---->
-
- <!---->
- </div>
- </div>
- </div>
- </section>
- </div>
- </template>
- </b-tab-stub>
- <b-tab-stub
- tag="div"
- title="NuGet"
- titlelinkclass="gl-tab-nav-item"
+ <div
+ class="svg-250 svg-content"
+ >
+ <img
+ alt=""
+ class="gl-max-w-full"
+ src="helpSvg"
+ />
+ </div>
+ </div>
+
+ <div
+ class="col-12"
>
- <template>
- <div>
- <section
- class="row empty-state text-center"
+ <div
+ class="text-content gl-mx-auto gl-my-0 gl-p-5"
+ >
+ <h1
+ class="h4"
+ >
+ There are no packages yet
+ </h1>
+
+ <p>
+ Learn how to
+ <b-link-stub
+ class="gl-link"
+ event="click"
+ href="helpUrl"
+ routertag="a"
+ target="_blank"
>
- <div
- class="col-12"
- >
- <div
- class="svg-250 svg-content"
- >
- <img
- alt=""
- class="gl-max-w-full"
- src="helpSvg"
- />
- </div>
- </div>
-
- <div
- class="col-12"
- >
- <div
- class="text-content gl-mx-auto gl-my-0 gl-p-5"
- >
- <h1
- class="h4"
- >
- There are no NuGet packages yet
- </h1>
-
- <p>
- Learn how to
- <b-link-stub
- class="gl-link"
- event="click"
- href="helpUrl"
- routertag="a"
- target="_blank"
- >
- publish and share your packages
- </b-link-stub>
- with GitLab.
- </p>
-
- <div>
- <!---->
-
- <!---->
- </div>
- </div>
- </div>
- </section>
- </div>
- </template>
- </b-tab-stub>
- <b-tab-stub
- tag="div"
- title="PyPI"
- titlelinkclass="gl-tab-nav-item"
- >
- <template>
+ publish and share your packages
+ </b-link-stub>
+ with GitLab.
+ </p>
+
<div>
- <section
- class="row empty-state text-center"
- >
- <div
- class="col-12"
- >
- <div
- class="svg-250 svg-content"
- >
- <img
- alt=""
- class="gl-max-w-full"
- src="helpSvg"
- />
- </div>
- </div>
-
- <div
- class="col-12"
- >
- <div
- class="text-content gl-mx-auto gl-my-0 gl-p-5"
- >
- <h1
- class="h4"
- >
- There are no PyPI packages yet
- </h1>
-
- <p>
- Learn how to
- <b-link-stub
- class="gl-link"
- event="click"
- href="helpUrl"
- routertag="a"
- target="_blank"
- >
- publish and share your packages
- </b-link-stub>
- with GitLab.
- </p>
-
- <div>
- <!---->
-
- <!---->
- </div>
- </div>
- </div>
- </section>
+ <!---->
+
+ <!---->
</div>
- </template>
- </b-tab-stub>
- </template>
- <template>
- <div
- class="gl-display-flex gl-align-self-center gl-py-2 gl-flex-grow-1 gl-justify-content-end"
- >
- <package-filter-stub
- class="gl-mr-2"
- />
-
- <package-sort-stub />
+ </div>
</div>
- </template>
- </b-tabs-stub>
+ </section>
+ </div>
</div>
`;
diff --git a/spec/frontend/packages/list/components/packages_filter_spec.js b/spec/frontend/packages/list/components/packages_filter_spec.js
deleted file mode 100644
index b186b5f5e48..00000000000
--- a/spec/frontend/packages/list/components/packages_filter_spec.js
+++ /dev/null
@@ -1,50 +0,0 @@
-import Vuex from 'vuex';
-import { GlSearchBoxByClick } from '@gitlab/ui';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
-import PackagesFilter from '~/packages/list/components/packages_filter.vue';
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
-
-describe('packages_filter', () => {
- let wrapper;
- let store;
-
- const findGlSearchBox = () => wrapper.find(GlSearchBoxByClick);
-
- const mountComponent = () => {
- store = new Vuex.Store();
- store.dispatch = jest.fn();
-
- wrapper = shallowMount(PackagesFilter, {
- localVue,
- store,
- });
- };
-
- beforeEach(mountComponent);
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- it('renders', () => {
- expect(wrapper.element).toMatchSnapshot();
- });
-
- describe('emits events', () => {
- it('sets the filter value in the store on input', () => {
- const searchString = 'foo';
- findGlSearchBox().vm.$emit('input', searchString);
-
- expect(store.dispatch).toHaveBeenCalledWith('setFilter', searchString);
- });
-
- it('emits the filter event when search box is submitted', () => {
- findGlSearchBox().vm.$emit('submit');
-
- expect(wrapper.emitted('filter')).toBeTruthy();
- });
- });
-});
diff --git a/spec/frontend/packages/list/components/packages_list_app_spec.js b/spec/frontend/packages/list/components/packages_list_app_spec.js
index 217096f822a..6862d23c4ff 100644
--- a/spec/frontend/packages/list/components/packages_list_app_spec.js
+++ b/spec/frontend/packages/list/components/packages_list_app_spec.js
@@ -1,11 +1,12 @@
-import Vuex from 'vuex';
+import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
-import { GlEmptyState, GlTab, GlTabs, GlSprintf, GlLink } from '@gitlab/ui';
-import * as commonUtils from '~/lib/utils/common_utils';
+import Vuex from 'vuex';
import createFlash from '~/flash';
+import * as commonUtils from '~/lib/utils/common_utils';
+import PackageSearch from '~/packages/list/components/package_search.vue';
import PackageListApp from '~/packages/list/components/packages_list_app.vue';
-import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages/list/constants';
+import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
jest.mock('~/lib/utils/common_utils');
jest.mock('~/flash');
@@ -26,9 +27,9 @@ describe('packages_list_app', () => {
const emptyListHelpUrl = 'helpUrl';
const findEmptyState = () => wrapper.find(GlEmptyState);
const findListComponent = () => wrapper.find(PackageList);
- const findTabComponent = (index = 0) => wrapper.findAll(GlTab).at(index);
+ const findPackageSearch = () => wrapper.find(PackageSearch);
- const createStore = (filterQuery = '') => {
+ const createStore = (filter = []) => {
store = new Vuex.Store({
state: {
isLoading: false,
@@ -38,7 +39,7 @@ describe('packages_list_app', () => {
emptyListHelpUrl,
packageHelpUrl: 'foo',
},
- filterQuery,
+ filter,
},
});
store.dispatch = jest.fn();
@@ -52,8 +53,6 @@ describe('packages_list_app', () => {
GlEmptyState,
GlLoadingIcon,
PackageList,
- GlTab,
- GlTabs,
GlSprintf,
GlLink,
},
@@ -94,6 +93,7 @@ describe('packages_list_app', () => {
it('call requestPackagesList on page:changed', () => {
mountComponent();
+ store.dispatch.mockClear();
const list = findListComponent();
list.vm.$emit('page:changed', 1);
@@ -108,41 +108,15 @@ describe('packages_list_app', () => {
expect(store.dispatch).toHaveBeenCalledWith('requestDeletePackage', 'foo');
});
- it('calls requestPackagesList on sort:changed', () => {
- mountComponent();
-
- const list = findListComponent();
- list.vm.$emit('sort:changed');
- expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList');
- });
-
it('does not call requestPackagesList two times on render', () => {
mountComponent();
expect(store.dispatch).toHaveBeenCalledTimes(1);
});
- describe('tab change', () => {
- it('calls requestPackagesList when all tab is clicked', () => {
- mountComponent();
-
- findTabComponent().trigger('click');
-
- expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList');
- });
-
- it('calls requestPackagesList when a package type tab is clicked', () => {
- mountComponent();
-
- findTabComponent(1).trigger('click');
-
- expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList');
- });
- });
-
describe('filter without results', () => {
beforeEach(() => {
- createStore('foo');
+ createStore([{ type: 'something' }]);
mountComponent();
});
@@ -154,12 +128,29 @@ describe('packages_list_app', () => {
});
});
+ describe('Package Search', () => {
+ it('exists', () => {
+ mountComponent();
+
+ expect(findPackageSearch().exists()).toBe(true);
+ });
+
+ it('on update fetches data from the store', () => {
+ mountComponent();
+ store.dispatch.mockClear();
+
+ findPackageSearch().vm.$emit('update');
+
+ expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList');
+ });
+ });
+
describe('delete alert handling', () => {
const { location } = window.location;
const search = `?${SHOW_DELETE_SUCCESS_ALERT}=true`;
beforeEach(() => {
- createStore('foo');
+ createStore();
jest.spyOn(commonUtils, 'historyReplaceState').mockImplementation(() => {});
delete window.location;
window.location = {
diff --git a/spec/frontend/packages/list/components/packages_list_spec.js b/spec/frontend/packages/list/components/packages_list_spec.js
index f981cc2851a..b1478a5e6dc 100644
--- a/spec/frontend/packages/list/components/packages_list_spec.js
+++ b/spec/frontend/packages/list/components/packages_list_spec.js
@@ -1,14 +1,14 @@
-import Vuex from 'vuex';
-import { last } from 'lodash';
import { GlTable, GlPagination, GlModal } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
+import { last } from 'lodash';
+import Vuex from 'vuex';
import stubChildren from 'helpers/stub_children';
-import Tracking from '~/tracking';
import PackagesList from '~/packages/list/components/packages_list.vue';
-import PackagesListLoader from '~/packages/shared/components/packages_list_loader.vue';
import PackagesListRow from '~/packages/shared/components/package_list_row.vue';
-import * as SharedUtils from '~/packages/shared/utils';
+import PackagesListLoader from '~/packages/shared/components/packages_list_loader.vue';
import { TrackingActions } from '~/packages/shared/constants';
+import * as SharedUtils from '~/packages/shared/utils';
+import Tracking from '~/tracking';
import { packageList } from '../../mock_data';
const localVue = createLocalVue();
diff --git a/spec/frontend/packages/list/components/packages_search_spec.js b/spec/frontend/packages/list/components/packages_search_spec.js
new file mode 100644
index 00000000000..9b62dde8d2b
--- /dev/null
+++ b/spec/frontend/packages/list/components/packages_search_spec.js
@@ -0,0 +1,107 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+import component from '~/packages/list/components/package_search.vue';
+import PackageTypeToken from '~/packages/list/components/tokens/package_type_token.vue';
+import getTableHeaders from '~/packages/list/utils';
+import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('Package Search', () => {
+ let wrapper;
+ let store;
+
+ const findRegistrySearch = () => wrapper.find(RegistrySearch);
+
+ const createStore = (isGroupPage) => {
+ const state = {
+ config: {
+ isGroupPage,
+ },
+ sorting: {
+ orderBy: 'version',
+ sort: 'desc',
+ },
+ filter: [],
+ };
+ store = new Vuex.Store({
+ state,
+ });
+ store.dispatch = jest.fn();
+ };
+
+ const mountComponent = (isGroupPage = false) => {
+ createStore(isGroupPage);
+
+ wrapper = shallowMount(component, {
+ localVue,
+ store,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('has a registry search component', () => {
+ mountComponent();
+
+ expect(findRegistrySearch().exists()).toBe(true);
+ expect(findRegistrySearch().props()).toMatchObject({
+ filter: store.state.filter,
+ sorting: store.state.sorting,
+ tokens: expect.arrayContaining([
+ expect.objectContaining({ token: PackageTypeToken, type: 'type', icon: 'package' }),
+ ]),
+ sortableFields: getTableHeaders(),
+ });
+ });
+
+ it.each`
+ isGroupPage | page
+ ${false} | ${'project'}
+ ${true} | ${'group'}
+ `('in a $page page binds the right props', ({ isGroupPage }) => {
+ mountComponent(isGroupPage);
+
+ expect(findRegistrySearch().props()).toMatchObject({
+ filter: store.state.filter,
+ sorting: store.state.sorting,
+ tokens: expect.arrayContaining([
+ expect.objectContaining({ token: PackageTypeToken, type: 'type', icon: 'package' }),
+ ]),
+ sortableFields: getTableHeaders(isGroupPage),
+ });
+ });
+
+ it('on sorting:changed emits update event and calls vuex setSorting', () => {
+ const payload = { sort: 'foo' };
+
+ mountComponent();
+
+ findRegistrySearch().vm.$emit('sorting:changed', payload);
+
+ expect(store.dispatch).toHaveBeenCalledWith('setSorting', payload);
+ expect(wrapper.emitted('update')).toEqual([[]]);
+ });
+
+ it('on filter:changed calls vuex setFilter', () => {
+ const payload = ['foo'];
+
+ mountComponent();
+
+ findRegistrySearch().vm.$emit('filter:changed', payload);
+
+ expect(store.dispatch).toHaveBeenCalledWith('setFilter', payload);
+ });
+
+ it('on filter:submit emits update event', () => {
+ mountComponent();
+
+ findRegistrySearch().vm.$emit('filter:submit');
+
+ expect(wrapper.emitted('update')).toEqual([[]]);
+ });
+});
diff --git a/spec/frontend/packages/list/components/packages_sort_spec.js b/spec/frontend/packages/list/components/packages_sort_spec.js
deleted file mode 100644
index d15ad9bd542..00000000000
--- a/spec/frontend/packages/list/components/packages_sort_spec.js
+++ /dev/null
@@ -1,90 +0,0 @@
-import Vuex from 'vuex';
-import { GlSorting, GlSortingItem } from '@gitlab/ui';
-import { mount, createLocalVue } from '@vue/test-utils';
-import stubChildren from 'helpers/stub_children';
-import PackagesSort from '~/packages/list/components/packages_sort.vue';
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
-
-describe('packages_sort', () => {
- let wrapper;
- let store;
- let sorting;
- let sortingItems;
-
- const findPackageListSorting = () => wrapper.find(GlSorting);
- const findSortingItems = () => wrapper.findAll(GlSortingItem);
-
- const createStore = (isGroupPage) => {
- const state = {
- config: {
- isGroupPage,
- },
- sorting: {
- orderBy: 'version',
- sort: 'desc',
- },
- };
- store = new Vuex.Store({
- state,
- });
- store.dispatch = jest.fn();
- };
-
- const mountComponent = (isGroupPage = false) => {
- createStore(isGroupPage);
-
- wrapper = mount(PackagesSort, {
- localVue,
- store,
- stubs: {
- ...stubChildren(PackagesSort),
- GlSortingItem,
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- describe('when is in projects', () => {
- beforeEach(() => {
- mountComponent();
- sorting = findPackageListSorting();
- sortingItems = findSortingItems();
- });
-
- it('has all the sortable items', () => {
- expect(sortingItems).toHaveLength(wrapper.vm.sortableFields.length);
- });
-
- it('on sort change set sorting in vuex and emit event', () => {
- sorting.vm.$emit('sortDirectionChange');
- expect(store.dispatch).toHaveBeenCalledWith('setSorting', { sort: 'asc' });
- expect(wrapper.emitted('sort:changed')).toBeTruthy();
- });
-
- it('on sort item click set sorting and emit event', () => {
- const item = sortingItems.at(0);
- const { orderBy } = wrapper.vm.sortableFields[0];
- item.vm.$emit('click');
- expect(store.dispatch).toHaveBeenCalledWith('setSorting', { orderBy });
- expect(wrapper.emitted('sort:changed')).toBeTruthy();
- });
- });
-
- describe('when is in group', () => {
- beforeEach(() => {
- mountComponent(true);
- sorting = findPackageListSorting();
- sortingItems = findSortingItems();
- });
-
- it('has all the sortable items', () => {
- expect(sortingItems).toHaveLength(wrapper.vm.sortableFields.length);
- });
- });
-});
diff --git a/spec/frontend/packages/list/components/packages_title_spec.js b/spec/frontend/packages/list/components/packages_title_spec.js
index 5e9ebd8ecb0..3716e8daa7c 100644
--- a/spec/frontend/packages/list/components/packages_title_spec.js
+++ b/spec/frontend/packages/list/components/packages_title_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
+import { LIST_INTRO_TEXT, LIST_TITLE_TEXT } from '~/packages/list//constants';
import PackageTitle from '~/packages/list/components/package_title.vue';
-import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
-import { LIST_INTRO_TEXT, LIST_TITLE_TEXT } from '~/packages/list//constants';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
describe('PackageTitle', () => {
let wrapper;
diff --git a/spec/frontend/packages/list/components/tokens/package_type_token_spec.js b/spec/frontend/packages/list/components/tokens/package_type_token_spec.js
new file mode 100644
index 00000000000..b0cbe34f0b9
--- /dev/null
+++ b/spec/frontend/packages/list/components/tokens/package_type_token_spec.js
@@ -0,0 +1,48 @@
+import { GlFilteredSearchToken, GlFilteredSearchSuggestion } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import component from '~/packages/list/components/tokens/package_type_token.vue';
+import { PACKAGE_TYPES } from '~/packages/list/constants';
+
+describe('packages_filter', () => {
+ let wrapper;
+
+ const findFilteredSearchToken = () => wrapper.find(GlFilteredSearchToken);
+ const findFilteredSearchSuggestions = () => wrapper.findAll(GlFilteredSearchSuggestion);
+
+ const mountComponent = ({ attrs, listeners } = {}) => {
+ wrapper = shallowMount(component, {
+ attrs,
+ listeners,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('it binds all of his attrs to filtered search token', () => {
+ mountComponent({ attrs: { foo: 'bar' } });
+
+ expect(findFilteredSearchToken().attributes('foo')).toBe('bar');
+ });
+
+ it('it binds all of his events to filtered search token', () => {
+ const clickListener = jest.fn();
+ mountComponent({ listeners: { click: clickListener } });
+
+ findFilteredSearchToken().vm.$emit('click');
+
+ expect(clickListener).toHaveBeenCalled();
+ });
+
+ it.each(PACKAGE_TYPES.map((p, index) => [p, index]))(
+ 'displays a suggestion for %p',
+ (packageType, index) => {
+ mountComponent();
+ const item = findFilteredSearchSuggestions().at(index);
+ expect(item.text()).toBe(packageType.title);
+ expect(item.props('value')).toBe(packageType.type);
+ },
+ );
+});
diff --git a/spec/frontend/packages/list/stores/actions_spec.js b/spec/frontend/packages/list/stores/actions_spec.js
index 05e1fe57cae..b5b0177eb4e 100644
--- a/spec/frontend/packages/list/stores/actions_spec.js
+++ b/spec/frontend/packages/list/stores/actions_spec.js
@@ -3,9 +3,9 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import { MISSING_DELETE_PATH_ERROR } from '~/packages/list/constants';
import * as actions from '~/packages/list/stores/actions';
import * as types from '~/packages/list/stores/mutation_types';
-import { MISSING_DELETE_PATH_ERROR } from '~/packages/list/constants';
import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages/shared/constants';
jest.mock('~/flash.js');
@@ -30,11 +30,13 @@ describe('Actions Package list store', () => {
sort: 'asc',
orderBy: 'version',
};
+
+ const filter = [];
it('should fetch the project packages list when isGroupPage is false', (done) => {
testAction(
actions.requestPackagesList,
undefined,
- { config: { isGroupPage: false, resourceId: 1 }, sorting },
+ { config: { isGroupPage: false, resourceId: 1 }, sorting, filter },
[],
[
{ type: 'setLoading', payload: true },
@@ -54,7 +56,7 @@ describe('Actions Package list store', () => {
testAction(
actions.requestPackagesList,
undefined,
- { config: { isGroupPage: true, resourceId: 2 }, sorting },
+ { config: { isGroupPage: true, resourceId: 2 }, sorting, filter },
[],
[
{ type: 'setLoading', payload: true },
@@ -70,7 +72,7 @@ describe('Actions Package list store', () => {
);
});
- it('should fetch packages of a certain type when selectedType is present', (done) => {
+ it('should fetch packages of a certain type when a filter with a type is present', (done) => {
const packageType = 'maven';
testAction(
@@ -79,7 +81,7 @@ describe('Actions Package list store', () => {
{
config: { isGroupPage: false, resourceId: 1 },
sorting,
- selectedType: { type: packageType },
+ filter: [{ type: 'type', value: { data: 'maven' } }],
},
[],
[
@@ -107,7 +109,7 @@ describe('Actions Package list store', () => {
testAction(
actions.requestPackagesList,
undefined,
- { config: { isGroupPage: false, resourceId: 2 }, sorting },
+ { config: { isGroupPage: false, resourceId: 2 }, sorting, filter },
[],
[
{ type: 'setLoading', payload: true },
diff --git a/spec/frontend/packages/list/stores/mutations_spec.js b/spec/frontend/packages/list/stores/mutations_spec.js
index 0d424a0c011..2ddf3a1da33 100644
--- a/spec/frontend/packages/list/stores/mutations_spec.js
+++ b/spec/frontend/packages/list/stores/mutations_spec.js
@@ -1,7 +1,7 @@
-import mutations from '~/packages/list/stores/mutations';
+import * as commonUtils from '~/lib/utils/common_utils';
import * as types from '~/packages/list/stores/mutation_types';
+import mutations from '~/packages/list/stores/mutations';
import createState from '~/packages/list/stores/state';
-import * as commonUtils from '~/lib/utils/common_utils';
import { npmPackage, mavenPackage } from '../../mock_data';
describe('Mutations Registry Store', () => {
@@ -78,17 +78,10 @@ describe('Mutations Registry Store', () => {
});
});
- describe('SET_SELECTED_TYPE', () => {
- it('should set the selected type', () => {
- mutations[types.SET_SELECTED_TYPE](mockState, { type: 'maven' });
- expect(mockState.selectedType).toEqual({ type: 'maven' });
- });
- });
-
describe('SET_FILTER', () => {
it('should set the filter query', () => {
mutations[types.SET_FILTER](mockState, 'foo');
- expect(mockState.filterQuery).toEqual('foo');
+ expect(mockState.filter).toEqual('foo');
});
});
});
diff --git a/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap b/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap
index 5faae5690db..4a75deebcf9 100644
--- a/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap
+++ b/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap
@@ -6,7 +6,7 @@ exports[`packages_list_row renders 1`] = `
data-qa-selector="package_row"
>
<div
- class="gl-display-flex gl-align-items-center gl-py-5"
+ class="gl-display-flex gl-align-items-center gl-py-3"
>
<!---->
@@ -14,7 +14,7 @@ exports[`packages_list_row renders 1`] = `
class="gl-display-flex gl-xs-flex-direction-column gl-justify-content-space-between gl-align-items-stretch gl-flex-fill-1"
>
<div
- class="gl-display-flex gl-flex-direction-column gl-justify-content-space-between gl-xs-mb-3 gl-min-w-0 gl-flex-grow-1"
+ class="gl-display-flex gl-flex-direction-column gl-xs-mb-3 gl-min-w-0 gl-flex-grow-1"
>
<div
class="gl-display-flex gl-align-items-center gl-text-body gl-font-weight-bold gl-min-h-6 gl-min-w-0"
@@ -40,7 +40,7 @@ exports[`packages_list_row renders 1`] = `
</div>
<div
- class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1 gl-min-h-6 gl-min-w-0 gl-flex-fill-1"
+ class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-min-h-6 gl-min-w-0 gl-flex-fill-1"
>
<div
class="gl-display-flex"
@@ -85,7 +85,7 @@ exports[`packages_list_row renders 1`] = `
</div>
<div
- class="gl-display-flex gl-align-items-center gl-mt-1 gl-min-h-6"
+ class="gl-display-flex gl-align-items-center gl-min-h-6"
>
<span>
<gl-sprintf-stub
@@ -97,7 +97,7 @@ exports[`packages_list_row renders 1`] = `
</div>
<div
- class="gl-w-9 gl-display-none gl-display-sm-flex gl-justify-content-end gl-pr-1"
+ class="gl-w-9 gl-display-none gl-sm-display-flex gl-justify-content-end gl-pr-1"
>
<gl-button-stub
aria-label="Remove package"
diff --git a/spec/frontend/packages/shared/components/package_list_row_spec.js b/spec/frontend/packages/shared/components/package_list_row_spec.js
index 0d0ea4e2122..bd122167273 100644
--- a/spec/frontend/packages/shared/components/package_list_row_spec.js
+++ b/spec/frontend/packages/shared/components/package_list_row_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import PackagesListRow from '~/packages/shared/components/package_list_row.vue';
-import PackageTags from '~/packages/shared/components/package_tags.vue';
import PackagePath from '~/packages/shared/components/package_path.vue';
+import PackageTags from '~/packages/shared/components/package_tags.vue';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import { packageList } from '../../mock_data';
diff --git a/spec/frontend/packages/shared/components/packages_list_loader_spec.js b/spec/frontend/packages/shared/components/packages_list_loader_spec.js
index 115a3a7095d..4ff01068f92 100644
--- a/spec/frontend/packages/shared/components/packages_list_loader_spec.js
+++ b/spec/frontend/packages/shared/components/packages_list_loader_spec.js
@@ -30,7 +30,7 @@ describe('PackagesListLoader', () => {
it('has the correct classes', () => {
expect(findDesktopShapes().classes()).toEqual([
'gl-display-none',
- 'gl-display-sm-flex',
+ 'gl-sm-display-flex',
'gl-flex-direction-column',
]);
});
@@ -44,7 +44,7 @@ describe('PackagesListLoader', () => {
it('has the correct classes', () => {
expect(findMobileShapes().classes()).toEqual([
'gl-flex-direction-column',
- 'gl-display-sm-none',
+ 'gl-sm-display-none',
]);
});
});
diff --git a/spec/frontend/packages/shared/utils_spec.js b/spec/frontend/packages/shared/utils_spec.js
index 590e3814bee..506f37f8895 100644
--- a/spec/frontend/packages/shared/utils_spec.js
+++ b/spec/frontend/packages/shared/utils_spec.js
@@ -1,10 +1,10 @@
+import { PackageType, TrackingCategories } from '~/packages/shared/constants';
import {
packageTypeToTrackCategory,
beautifyPath,
getPackageTypeLabel,
getCommitLink,
} from '~/packages/shared/utils';
-import { PackageType, TrackingCategories } from '~/packages/shared/constants';
import { packageList } from '../mock_data';
describe('Packages shared utils', () => {
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
new file mode 100644
index 00000000000..be0d7114e6e
--- /dev/null
+++ b/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js
@@ -0,0 +1,309 @@
+import { GlSprintf, GlLink, GlAlert } from '@gitlab/ui';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import component from '~/packages_and_registries/settings/group/components/group_settings_app.vue';
+import MavenSettings from '~/packages_and_registries/settings/group/components/maven_settings.vue';
+import {
+ PACKAGE_SETTINGS_HEADER,
+ PACKAGE_SETTINGS_DESCRIPTION,
+ PACKAGES_DOCS_PATH,
+ ERROR_UPDATING_SETTINGS,
+ SUCCESS_UPDATING_SETTINGS,
+} 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 {
+ groupPackageSettingsMock,
+ groupPackageSettingsMutationMock,
+ groupPackageSettingsMutationErrorMock,
+} from '../mock_data';
+
+jest.mock('~/flash');
+
+const localVue = createLocalVue();
+
+describe('Group Settings App', () => {
+ let wrapper;
+ let apolloProvider;
+ let show;
+
+ const defaultProvide = {
+ defaultExpanded: false,
+ groupPath: 'foo_group_path',
+ };
+
+ const mountComponent = ({
+ provide = defaultProvide,
+ resolver = jest.fn().mockResolvedValue(groupPackageSettingsMock),
+ mutationResolver = jest.fn().mockResolvedValue(groupPackageSettingsMutationMock()),
+ data = {},
+ } = {}) => {
+ localVue.use(VueApollo);
+
+ const requestHandlers = [
+ [getGroupPackagesSettingsQuery, resolver],
+ [updateNamespacePackageSettings, mutationResolver],
+ ];
+
+ apolloProvider = createMockApollo(requestHandlers);
+
+ wrapper = shallowMount(component, {
+ localVue,
+ apolloProvider,
+ provide,
+ data() {
+ return {
+ ...data,
+ };
+ },
+ stubs: {
+ GlSprintf,
+ SettingsBlock,
+ },
+ mocks: {
+ $toast: {
+ show,
+ },
+ },
+ });
+ };
+
+ beforeEach(() => {
+ show = jest.fn();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findSettingsBlock = () => wrapper.find(SettingsBlock);
+ const findDescription = () => wrapper.find('[data-testid="description"');
+ const findLink = () => wrapper.find(GlLink);
+ const findMavenSettings = () => wrapper.find(MavenSettings);
+ const findAlert = () => wrapper.find(GlAlert);
+
+ const waitForApolloQueryAndRender = async () => {
+ await waitForPromises();
+ await wrapper.vm.$nextTick();
+ };
+
+ const emitSettingsUpdate = (override) => {
+ findMavenSettings().vm.$emit('update', {
+ mavenDuplicateExceptionRegex: ')',
+ ...override,
+ });
+ };
+
+ it('renders a settings block', () => {
+ mountComponent();
+
+ expect(findSettingsBlock().exists()).toBe(true);
+ });
+
+ it('passes the correct props to settings block', () => {
+ mountComponent();
+
+ expect(findSettingsBlock().props('defaultExpanded')).toBe(false);
+ });
+
+ it('has the correct header text', () => {
+ mountComponent();
+
+ expect(wrapper.text()).toContain(PACKAGE_SETTINGS_HEADER);
+ });
+
+ it('has the correct description text', () => {
+ mountComponent();
+
+ expect(findDescription().text()).toMatchInterpolatedText(PACKAGE_SETTINGS_DESCRIPTION);
+ });
+
+ it('has the correct link', () => {
+ mountComponent();
+
+ expect(findLink().attributes()).toMatchObject({
+ href: PACKAGES_DOCS_PATH,
+ target: '_blank',
+ });
+ expect(findLink().text()).toBe('More Information');
+ });
+
+ it('calls the graphql API with the proper variables', () => {
+ const resolver = jest.fn().mockResolvedValue(groupPackageSettingsMock);
+ mountComponent({ resolver });
+
+ expect(resolver).toHaveBeenCalledWith({
+ fullPath: defaultProvide.groupPath,
+ });
+ });
+
+ describe('maven settings', () => {
+ it('exists', () => {
+ mountComponent();
+
+ expect(findMavenSettings().exists()).toBe(true);
+ });
+
+ it('assigns duplication allowness and exception props', async () => {
+ mountComponent();
+
+ expect(findMavenSettings().props('loading')).toBe(true);
+
+ await waitForApolloQueryAndRender();
+
+ const {
+ mavenDuplicatesAllowed,
+ mavenDuplicateExceptionRegex,
+ } = groupPackageSettingsMock.data.group.packageSettings;
+
+ expect(findMavenSettings().props()).toMatchObject({
+ mavenDuplicatesAllowed,
+ mavenDuplicateExceptionRegex,
+ mavenDuplicateExceptionRegexError: '',
+ loading: false,
+ });
+ });
+
+ it('on update event calls the mutation', async () => {
+ const mutationResolver = jest.fn().mockResolvedValue(groupPackageSettingsMutationMock());
+ mountComponent({ mutationResolver });
+
+ await waitForApolloQueryAndRender();
+
+ emitSettingsUpdate();
+
+ expect(mutationResolver).toHaveBeenCalledWith({
+ input: { mavenDuplicateExceptionRegex: ')', namespacePath: 'foo_group_path' },
+ });
+ });
+ });
+
+ describe('settings update', () => {
+ describe('success state', () => {
+ it('shows a success alert', async () => {
+ mountComponent();
+
+ await waitForApolloQueryAndRender();
+
+ emitSettingsUpdate();
+
+ await waitForPromises();
+
+ expect(show).toHaveBeenCalledWith(SUCCESS_UPDATING_SETTINGS, {
+ type: 'success',
+ });
+ });
+
+ it('has an optimistic response', async () => {
+ const mavenDuplicateExceptionRegex = 'latest[master]something';
+ mountComponent();
+
+ await waitForApolloQueryAndRender();
+
+ expect(findMavenSettings().props('mavenDuplicateExceptionRegex')).toBe('');
+
+ emitSettingsUpdate({ mavenDuplicateExceptionRegex });
+
+ // wait for apollo to update the model with the optimistic response
+ await wrapper.vm.$nextTick();
+
+ expect(findMavenSettings().props('mavenDuplicateExceptionRegex')).toBe(
+ mavenDuplicateExceptionRegex,
+ );
+
+ // wait for the call to resolve
+ await waitForPromises();
+
+ expect(findMavenSettings().props('mavenDuplicateExceptionRegex')).toBe(
+ mavenDuplicateExceptionRegex,
+ );
+ });
+ });
+
+ describe('errors', () => {
+ const verifyAlert = () => {
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(ERROR_UPDATING_SETTINGS);
+ expect(findAlert().props('variant')).toBe('warning');
+ };
+
+ it('mutation payload with root level errors', async () => {
+ // note this is a complex test that covers all the path around errors that are shown in the form
+ // it's one single it case, due to the expensive preparation and execution
+ const mutationResolver = jest.fn().mockResolvedValue(groupPackageSettingsMutationErrorMock);
+ mountComponent({ mutationResolver });
+
+ await waitForApolloQueryAndRender();
+
+ emitSettingsUpdate();
+
+ await waitForApolloQueryAndRender();
+
+ // errors are bound to the component
+ expect(findMavenSettings().props('mavenDuplicateExceptionRegexError')).toBe(
+ groupPackageSettingsMutationErrorMock.errors[0].extensions.problems[0].message,
+ );
+
+ // general error message is shown
+
+ verifyAlert();
+
+ emitSettingsUpdate();
+
+ await wrapper.vm.$nextTick();
+
+ // errors are reset on mutation call
+ expect(findMavenSettings().props('mavenDuplicateExceptionRegexError')).toBe('');
+ });
+
+ it.each`
+ type | mutationResolver
+ ${'local'} | ${jest.fn().mockResolvedValue(groupPackageSettingsMutationMock({ errors: ['foo'] }))}
+ ${'network'} | ${jest.fn().mockRejectedValue()}
+ `('mutation payload with $type error', async ({ mutationResolver }) => {
+ mountComponent({ mutationResolver });
+
+ await waitForApolloQueryAndRender();
+
+ emitSettingsUpdate();
+
+ await waitForPromises();
+
+ verifyAlert();
+ });
+
+ it('a successful request dismisses the alert', async () => {
+ mountComponent({ data: { alertMessage: 'foo' } });
+
+ await waitForApolloQueryAndRender();
+
+ expect(findAlert().exists()).toBe(true);
+
+ emitSettingsUpdate();
+
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(false);
+ });
+
+ it('dismiss event from alert dismiss it from the page', async () => {
+ mountComponent({ data: { alertMessage: 'foo' } });
+
+ await waitForApolloQueryAndRender();
+
+ expect(findAlert().exists()).toBe(true);
+
+ findAlert().vm.$emit('dismiss');
+
+ await wrapper.vm.$nextTick();
+
+ expect(findAlert().exists()).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/settings/group/components/maven_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/maven_settings_spec.js
new file mode 100644
index 00000000000..2433c50ff24
--- /dev/null
+++ b/spec/frontend/packages_and_registries/settings/group/components/maven_settings_spec.js
@@ -0,0 +1,153 @@
+import { GlSprintf, GlToggle, GlFormGroup, GlFormInput } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import component from '~/packages_and_registries/settings/group/components/maven_settings.vue';
+
+import {
+ MAVEN_TITLE,
+ MAVEN_SETTINGS_SUBTITLE,
+ MAVEN_DUPLICATES_ALLOWED_DISABLED,
+ MAVEN_DUPLICATES_ALLOWED_ENABLED,
+ MAVEN_SETTING_EXCEPTION_TITLE,
+ MAVEN_SETTINGS_EXCEPTION_LEGEND,
+} from '~/packages_and_registries/settings/group/constants';
+
+describe('Maven Settings', () => {
+ let wrapper;
+
+ const defaultProps = {
+ mavenDuplicatesAllowed: false,
+ mavenDuplicateExceptionRegex: 'foo',
+ };
+
+ const mountComponent = (propsData = defaultProps) => {
+ wrapper = shallowMount(component, {
+ propsData,
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findTitle = () => wrapper.find('h5');
+ const findSubTitle = () => wrapper.find('p');
+ const findToggle = () => wrapper.find(GlToggle);
+ const findToggleLabel = () => wrapper.find('[data-testid="toggle-label"');
+
+ const findInputGroup = () => wrapper.find(GlFormGroup);
+ const findInput = () => wrapper.find(GlFormInput);
+
+ it('has a title', () => {
+ mountComponent();
+
+ expect(findTitle().exists()).toBe(true);
+ expect(findTitle().text()).toBe(MAVEN_TITLE);
+ });
+
+ it('has a subtitle', () => {
+ mountComponent();
+
+ expect(findSubTitle().exists()).toBe(true);
+ expect(findSubTitle().text()).toBe(MAVEN_SETTINGS_SUBTITLE);
+ });
+
+ it('has a toggle', () => {
+ mountComponent();
+
+ expect(findToggle().exists()).toBe(true);
+ expect(findToggle().props('value')).toBe(defaultProps.mavenDuplicatesAllowed);
+ });
+
+ it('toggle emits an update event', () => {
+ mountComponent();
+
+ findToggle().vm.$emit('change', false);
+
+ expect(wrapper.emitted('update')).toEqual([[{ mavenDuplicatesAllowed: false }]]);
+ });
+
+ describe('when the duplicates are disabled', () => {
+ it('the toggle has the disabled message', () => {
+ mountComponent();
+
+ expect(findToggleLabel().exists()).toBe(true);
+ expect(findToggleLabel().text()).toMatchInterpolatedText(MAVEN_DUPLICATES_ALLOWED_DISABLED);
+ });
+
+ it('shows a form group with an input field', () => {
+ mountComponent();
+
+ expect(findInputGroup().exists()).toBe(true);
+
+ expect(findInputGroup().attributes()).toMatchObject({
+ 'label-for': 'maven-duplicated-settings-regex-input',
+ label: MAVEN_SETTING_EXCEPTION_TITLE,
+ description: MAVEN_SETTINGS_EXCEPTION_LEGEND,
+ });
+ });
+
+ it('shows an input field', () => {
+ mountComponent();
+
+ expect(findInput().exists()).toBe(true);
+
+ expect(findInput().attributes()).toMatchObject({
+ id: 'maven-duplicated-settings-regex-input',
+ value: defaultProps.mavenDuplicateExceptionRegex,
+ });
+ });
+
+ it('input change event emits an update event', () => {
+ mountComponent();
+
+ findInput().vm.$emit('change', 'bar');
+
+ expect(wrapper.emitted('update')).toEqual([[{ mavenDuplicateExceptionRegex: 'bar' }]]);
+ });
+
+ describe('valid state', () => {
+ it('form group has correct props', () => {
+ mountComponent();
+
+ expect(findInputGroup().attributes()).toMatchObject({
+ state: 'true',
+ 'invalid-feedback': '',
+ });
+ });
+ });
+
+ describe('invalid state', () => {
+ it('form group has correct props', () => {
+ const propsWithError = {
+ ...defaultProps,
+ mavenDuplicateExceptionRegexError: 'some error string',
+ };
+
+ mountComponent(propsWithError);
+
+ expect(findInputGroup().attributes()).toMatchObject({
+ 'invalid-feedback': propsWithError.mavenDuplicateExceptionRegexError,
+ });
+ });
+ });
+ });
+
+ describe('when the duplicates are enabled', () => {
+ it('has the correct toggle label', () => {
+ mountComponent({ ...defaultProps, mavenDuplicatesAllowed: true });
+
+ expect(findToggleLabel().exists()).toBe(true);
+ expect(findToggleLabel().text()).toMatchInterpolatedText(MAVEN_DUPLICATES_ALLOWED_ENABLED);
+ });
+
+ it('hides the form input group', () => {
+ mountComponent({ ...defaultProps, mavenDuplicatesAllowed: true });
+
+ expect(findInputGroup().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/settings/group/graphl/utils/cache_update_spec.js b/spec/frontend/packages_and_registries/settings/group/graphl/utils/cache_update_spec.js
new file mode 100644
index 00000000000..e1a46f97318
--- /dev/null
+++ b/spec/frontend/packages_and_registries/settings/group/graphl/utils/cache_update_spec.js
@@ -0,0 +1,56 @@
+import expirationPolicyQuery from '~/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql';
+import { updateGroupPackageSettings } from '~/packages_and_registries/settings/group/graphql/utils/cache_update';
+
+describe('Package and Registries settings group cache updates', () => {
+ let client;
+
+ const payload = {
+ data: {
+ updateNamespacePackageSettings: {
+ packageSettings: {
+ mavenDuplicatesAllowed: false,
+ mavenDuplicateExceptionRegex: 'latest[master]something',
+ },
+ },
+ },
+ };
+
+ const cacheMock = {
+ group: {
+ packageSettings: {
+ mavenDuplicatesAllowed: true,
+ mavenDuplicateExceptionRegex: '',
+ },
+ },
+ };
+
+ const queryAndVariables = {
+ query: expirationPolicyQuery,
+ variables: { fullPath: 'foo' },
+ };
+
+ beforeEach(() => {
+ client = {
+ readQuery: jest.fn().mockReturnValue(cacheMock),
+ writeQuery: jest.fn(),
+ };
+ });
+ describe('updateGroupPackageSettings', () => {
+ it('calls readQuery', () => {
+ updateGroupPackageSettings('foo')(client, payload);
+ expect(client.readQuery).toHaveBeenCalledWith(queryAndVariables);
+ });
+
+ it('writes the correct result in the cache', () => {
+ updateGroupPackageSettings('foo')(client, payload);
+ expect(client.writeQuery).toHaveBeenCalledWith({
+ ...queryAndVariables,
+ data: {
+ group: {
+ ...payload.data.updateNamespacePackageSettings,
+ },
+ },
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/settings/group/graphl/utils/optimistic_responses_spec.js b/spec/frontend/packages_and_registries/settings/group/graphl/utils/optimistic_responses_spec.js
new file mode 100644
index 00000000000..a3c53d5768a
--- /dev/null
+++ b/spec/frontend/packages_and_registries/settings/group/graphl/utils/optimistic_responses_spec.js
@@ -0,0 +1,20 @@
+import { updateGroupPackagesSettingsOptimisticResponse } from '~/packages_and_registries/settings/group/graphql/utils/optimistic_responses';
+
+describe('Optimistic responses', () => {
+ describe('updateGroupPackagesSettingsOptimisticResponse', () => {
+ it('returns the correct structure', () => {
+ expect(updateGroupPackagesSettingsOptimisticResponse({ foo: 'bar' })).toMatchInlineSnapshot(`
+ Object {
+ "__typename": "Mutation",
+ "updateNamespacePackageSettings": Object {
+ "__typename": "UpdateNamespacePackageSettingsPayload",
+ "errors": Array [],
+ "packageSettings": Object {
+ "foo": "bar",
+ },
+ },
+ }
+ `);
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/settings/group/mock_data.js b/spec/frontend/packages_and_registries/settings/group/mock_data.js
new file mode 100644
index 00000000000..777c0898de0
--- /dev/null
+++ b/spec/frontend/packages_and_registries/settings/group/mock_data.js
@@ -0,0 +1,48 @@
+export const groupPackageSettingsMock = {
+ data: {
+ group: {
+ packageSettings: {
+ mavenDuplicatesAllowed: true,
+ mavenDuplicateExceptionRegex: '',
+ },
+ },
+ },
+};
+
+export const groupPackageSettingsMutationMock = (override) => ({
+ data: {
+ updateNamespacePackageSettings: {
+ packageSettings: {
+ mavenDuplicatesAllowed: true,
+ mavenDuplicateExceptionRegex: 'latest[master]something',
+ },
+ errors: [],
+ ...override,
+ },
+ },
+});
+
+export const groupPackageSettingsMutationErrorMock = {
+ errors: [
+ {
+ message:
+ 'Variable $input of type UpdateNamespacePackageSettingsInput! was provided invalid value for mavenDuplicateExceptionRegex (latest[master]somethingj)) is an invalid regexp: unexpected ): latest[master]somethingj)))',
+ locations: [{ line: 1, column: 41 }],
+ extensions: {
+ value: {
+ namespacePath: 'gitlab-org',
+ mavenDuplicateExceptionRegex: 'latest[master]something))',
+ },
+ problems: [
+ {
+ path: ['mavenDuplicateExceptionRegex'],
+ explanation:
+ 'latest[master]somethingj)) is an invalid regexp: unexpected ): latest[master]something))',
+ message:
+ 'latest[master]somethingj)) is an invalid regexp: unexpected ): latest[master]something))',
+ },
+ ],
+ },
+ },
+ ],
+};
diff --git a/spec/frontend/pager_spec.js b/spec/frontend/pager_spec.js
index c8a129e38e7..ad4222e7cb2 100644
--- a/spec/frontend/pager_spec.js
+++ b/spec/frontend/pager_spec.js
@@ -1,9 +1,9 @@
-import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter';
+import $ from 'jquery';
import { TEST_HOST } from 'helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
-import Pager from '~/pager';
import { removeParams } from '~/lib/utils/url_utility';
+import Pager from '~/pager';
jest.mock('~/lib/utils/url_utility', () => ({
removeParams: jest.fn().mockName('removeParams'),
diff --git a/spec/frontend/pages/admin/jobs/index/components/stop_jobs_modal_spec.js b/spec/frontend/pages/admin/jobs/index/components/stop_jobs_modal_spec.js
index 81750b4827f..52648d3ce00 100644
--- a/spec/frontend/pages/admin/jobs/index/components/stop_jobs_modal_spec.js
+++ b/spec/frontend/pages/admin/jobs/index/components/stop_jobs_modal_spec.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
import { TEST_HOST } from 'helpers/test_constants';
-import { redirectTo } from '~/lib/utils/url_utility';
+import mountComponent from 'helpers/vue_mount_component_helper';
import axios from '~/lib/utils/axios_utils';
+import { redirectTo } from '~/lib/utils/url_utility';
import stopJobsModal from '~/pages/admin/jobs/index/components/stop_jobs_modal.vue';
jest.mock('~/lib/utils/url_utility', () => ({
diff --git a/spec/frontend/pages/admin/users/components/delete_user_modal_spec.js b/spec/frontend/pages/admin/users/components/delete_user_modal_spec.js
index d203a8ea0e0..c7293b00adf 100644
--- a/spec/frontend/pages/admin/users/components/delete_user_modal_spec.js
+++ b/spec/frontend/pages/admin/users/components/delete_user_modal_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlButton, GlFormInput } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import DeleteUserModal from '~/pages/admin/users/components/delete_user_modal.vue';
import ModalStub from './stubs/modal_stub';
diff --git a/spec/frontend/pages/admin/users/components/user_modal_manager_spec.js b/spec/frontend/pages/admin/users/components/user_modal_manager_spec.js
index 6df2efd624d..3669bc40d7e 100644
--- a/spec/frontend/pages/admin/users/components/user_modal_manager_spec.js
+++ b/spec/frontend/pages/admin/users/components/user_modal_manager_spec.js
@@ -3,6 +3,8 @@ import UserModalManager from '~/pages/admin/users/components/user_modal_manager.
import ModalStub from './stubs/modal_stub';
describe('Users admin page Modal Manager', () => {
+ let wrapper;
+
const modalConfiguration = {
action1: {
title: 'action1',
@@ -14,11 +16,12 @@ describe('Users admin page Modal Manager', () => {
},
};
- let wrapper;
+ const findModal = () => wrapper.find({ ref: 'modal' });
const createComponent = (props = {}) => {
wrapper = mount(UserModalManager, {
propsData: {
+ selector: '.js-delete-user-modal-button',
modalConfiguration,
csrfToken: 'dummyCSRF',
...props,
@@ -37,7 +40,7 @@ describe('Users admin page Modal Manager', () => {
describe('render behavior', () => {
it('does not renders modal when initialized', () => {
createComponent();
- expect(wrapper.find({ ref: 'modal' }).exists()).toBeFalsy();
+ expect(findModal().exists()).toBeFalsy();
});
it('throws if action has no proper configuration', () => {
@@ -55,7 +58,7 @@ describe('Users admin page Modal Manager', () => {
});
return wrapper.vm.$nextTick().then(() => {
- const modal = wrapper.find({ ref: 'modal' });
+ const modal = findModal();
expect(modal.exists()).toBeTruthy();
expect(modal.vm.$attrs.csrfToken).toEqual('dummyCSRF');
expect(modal.vm.$attrs.extraProp).toEqual('extraPropValue');
@@ -64,68 +67,60 @@ describe('Users admin page Modal Manager', () => {
});
});
- describe('global listener', () => {
+ describe('click handling', () => {
+ let button;
+ let button2;
+
+ const createButtons = () => {
+ button = document.createElement('button');
+ button2 = document.createElement('button');
+ button.setAttribute('class', 'js-delete-user-modal-button');
+ button.setAttribute('data-username', 'foo');
+ button.setAttribute('data-gl-modal-action', 'action1');
+ button.setAttribute('data-block-user-url', '/block');
+ button.setAttribute('data-delete-user-url', '/delete');
+ document.body.appendChild(button);
+ document.body.appendChild(button2);
+ };
+ const removeButtons = () => {
+ button.remove();
+ button = null;
+ button2.remove();
+ button2 = null;
+ };
+
beforeEach(() => {
- jest.spyOn(document, 'addEventListener');
- jest.spyOn(document, 'removeEventListener');
+ createButtons();
+ createComponent();
});
- afterAll(() => {
- jest.restoreAllMocks();
+ afterEach(() => {
+ removeButtons();
});
- it('registers global listener on mount', () => {
- createComponent();
- expect(document.addEventListener).toHaveBeenCalledWith('click', expect.any(Function));
- });
+ it('renders the modal when the button is clicked', async () => {
+ button.click();
- it('removes global listener on destroy', () => {
- createComponent();
- wrapper.destroy();
- expect(document.removeEventListener).toHaveBeenCalledWith('click', expect.any(Function));
+ await wrapper.vm.$nextTick();
+
+ expect(findModal().exists()).toBe(true);
});
- });
- describe('click handling', () => {
- let node;
+ it('does not render the modal when a misconfigured button is clicked', async () => {
+ button.removeAttribute('data-gl-modal-action');
+ button.click();
- beforeEach(() => {
- node = document.createElement('div');
- document.body.appendChild(node);
- });
+ await wrapper.vm.$nextTick();
- afterEach(() => {
- node.remove();
- node = null;
+ expect(findModal().exists()).toBe(false);
});
- it('ignores wrong clicks', () => {
- createComponent();
- const event = new window.MouseEvent('click', {
- bubbles: true,
- cancellable: true,
- });
- jest.spyOn(event, 'preventDefault');
- node.dispatchEvent(event);
- expect(event.preventDefault).not.toHaveBeenCalled();
- });
+ it('does not render the modal when a button without the selector class is clicked', async () => {
+ button2.click();
- it('captures click with glModalAction', () => {
- createComponent();
- node.dataset.glModalAction = 'action1';
- const event = new window.MouseEvent('click', {
- bubbles: true,
- cancellable: true,
- });
- jest.spyOn(event, 'preventDefault');
- node.dispatchEvent(event);
+ await wrapper.vm.$nextTick();
- expect(event.preventDefault).toHaveBeenCalled();
- return wrapper.vm.$nextTick().then(() => {
- const modal = wrapper.find({ ref: 'modal' });
- expect(modal.exists()).toBeTruthy();
- expect(modal.vm.showWasCalled).toBeTruthy();
- });
+ expect(findModal().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/pages/dashboard/projects/index/components/customize_homepage_banner_spec.js b/spec/frontend/pages/dashboard/projects/index/components/customize_homepage_banner_spec.js
index fbe2274c40d..63c1260560b 100644
--- a/spec/frontend/pages/dashboard/projects/index/components/customize_homepage_banner_spec.js
+++ b/spec/frontend/pages/dashboard/projects/index/components/customize_homepage_banner_spec.js
@@ -1,9 +1,9 @@
-import { shallowMount } from '@vue/test-utils';
import { GlBanner } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { mockTracking, unmockTracking, triggerEvent } from 'helpers/tracking_helper';
-import CustomizeHomepageBanner from '~/pages/dashboard/projects/index/components/customize_homepage_banner.vue';
import axios from '~/lib/utils/axios_utils';
+import CustomizeHomepageBanner from '~/pages/dashboard/projects/index/components/customize_homepage_banner.vue';
const svgPath = '/illustrations/background';
const provide = {
diff --git a/spec/frontend/pages/dashboard/todos/index/todos_spec.js b/spec/frontend/pages/dashboard/todos/index/todos_spec.js
index 5018b0c4f73..fb612f17669 100644
--- a/spec/frontend/pages/dashboard/todos/index/todos_spec.js
+++ b/spec/frontend/pages/dashboard/todos/index/todos_spec.js
@@ -1,10 +1,10 @@
-import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter';
-import Todos from '~/pages/dashboard/todos/index/todos';
+import $ from 'jquery';
import '~/lib/utils/common_utils';
import axios from '~/lib/utils/axios_utils';
import { addDelimiter } from '~/lib/utils/text_utility';
import { visitUrl } from '~/lib/utils/url_utility';
+import Todos from '~/pages/dashboard/todos/index/todos';
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn().mockName('visitUrl'),
diff --git a/spec/frontend/pages/import/bitbucket_server/components/bitbucket_server_status_table_spec.js b/spec/frontend/pages/import/bitbucket_server/components/bitbucket_server_status_table_spec.js
index a91fc5abe09..7a8a249cb2a 100644
--- a/spec/frontend/pages/import/bitbucket_server/components/bitbucket_server_status_table_spec.js
+++ b/spec/frontend/pages/import/bitbucket_server/components/bitbucket_server_status_table_spec.js
@@ -1,8 +1,8 @@
+import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { GlButton } from '@gitlab/ui';
-import BitbucketServerStatusTable from '~/pages/import/bitbucket_server/status/components/bitbucket_server_status_table.vue';
import BitbucketStatusTable from '~/import_entities/import_projects/components/bitbucket_status_table.vue';
+import BitbucketServerStatusTable from '~/pages/import/bitbucket_server/status/components/bitbucket_server_status_table.vue';
const BitbucketStatusTableStub = {
name: 'BitbucketStatusTable',
diff --git a/spec/frontend/pages/labels/components/promote_label_modal_spec.js b/spec/frontend/pages/labels/components/promote_label_modal_spec.js
index 19807313c77..4d5d1f98b59 100644
--- a/spec/frontend/pages/labels/components/promote_label_modal_spec.js
+++ b/spec/frontend/pages/labels/components/promote_label_modal_spec.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
import { TEST_HOST } from 'helpers/test_constants';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import axios from '~/lib/utils/axios_utils';
import promoteLabelModal from '~/pages/projects/labels/components/promote_label_modal.vue';
import eventHub from '~/pages/projects/labels/event_hub';
-import axios from '~/lib/utils/axios_utils';
describe('Promote label modal', () => {
let vm;
diff --git a/spec/frontend/pages/milestones/shared/components/delete_milestone_modal_spec.js b/spec/frontend/pages/milestones/shared/components/delete_milestone_modal_spec.js
index 7bb637356c2..1fbec0d996d 100644
--- a/spec/frontend/pages/milestones/shared/components/delete_milestone_modal_spec.js
+++ b/spec/frontend/pages/milestones/shared/components/delete_milestone_modal_spec.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
import { TEST_HOST } from 'helpers/test_constants';
-import { redirectTo } from '~/lib/utils/url_utility';
+import mountComponent from 'helpers/vue_mount_component_helper';
import axios from '~/lib/utils/axios_utils';
+import { redirectTo } from '~/lib/utils/url_utility';
import deleteMilestoneModal from '~/pages/milestones/shared/components/delete_milestone_modal.vue';
import eventHub from '~/pages/milestones/shared/event_hub';
diff --git a/spec/frontend/pages/milestones/shared/components/promote_milestone_modal_spec.js b/spec/frontend/pages/milestones/shared/components/promote_milestone_modal_spec.js
index 7cd94deb3da..d22e0474e06 100644
--- a/spec/frontend/pages/milestones/shared/components/promote_milestone_modal_spec.js
+++ b/spec/frontend/pages/milestones/shared/components/promote_milestone_modal_spec.js
@@ -1,12 +1,12 @@
import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { TEST_HOST } from 'helpers/test_constants';
import { setHTMLFixture } from 'helpers/fixtures';
+import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
-import PromoteMilestoneModal from '~/pages/milestones/shared/components/promote_milestone_modal.vue';
+import * as flash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as urlUtils from '~/lib/utils/url_utility';
-import * as flash from '~/flash';
+import PromoteMilestoneModal from '~/pages/milestones/shared/components/promote_milestone_modal.vue';
jest.mock('~/lib/utils/url_utility');
jest.mock('~/flash');
diff --git a/spec/frontend/pages/projects/edit/mount_search_settings_spec.js b/spec/frontend/pages/projects/edit/mount_search_settings_spec.js
deleted file mode 100644
index b48809b3d00..00000000000
--- a/spec/frontend/pages/projects/edit/mount_search_settings_spec.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import initSearch from '~/search_settings';
-import mountSearchSettings from '~/pages/projects/edit/mount_search_settings';
-
-jest.mock('~/search_settings');
-
-describe('pages/projects/edit/mount_search_settings', () => {
- afterEach(() => {
- resetHTMLFixture();
- });
-
- it('initializes search settings when js-search-settings-app is available', async () => {
- setHTMLFixture('<div class="js-search-settings-app"></div>');
-
- await mountSearchSettings();
-
- expect(initSearch).toHaveBeenCalled();
- });
-
- it('does not initialize search settings when js-search-settings-app is unavailable', async () => {
- await mountSearchSettings();
-
- expect(initSearch).not.toHaveBeenCalled();
- });
-});
diff --git a/spec/frontend/pages/projects/forks/new/components/fork_groups_list_item_spec.js b/spec/frontend/pages/projects/forks/new/components/fork_groups_list_item_spec.js
index b90c07a335b..b5425fa6f2e 100644
--- a/spec/frontend/pages/projects/forks/new/components/fork_groups_list_item_spec.js
+++ b/spec/frontend/pages/projects/forks/new/components/fork_groups_list_item_spec.js
@@ -1,14 +1,10 @@
-import { shallowMount } from '@vue/test-utils';
import { GlBadge, GlButton, GlLink } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import ForkGroupsListItem from '~/pages/projects/forks/new/components/fork_groups_list_item.vue';
describe('Fork groups list item component', () => {
let wrapper;
- const DEFAULT_PROPS = {
- hasReachedProjectLimit: false,
- };
-
const DEFAULT_GROUP_DATA = {
id: 22,
name: 'Gitlab Org',
@@ -33,7 +29,6 @@ describe('Fork groups list item component', () => {
const createWrapper = (propsData) => {
wrapper = shallowMount(ForkGroupsListItem, {
propsData: {
- ...DEFAULT_PROPS,
...propsData,
},
});
diff --git a/spec/frontend/pages/projects/forks/new/components/fork_groups_list_spec.js b/spec/frontend/pages/projects/forks/new/components/fork_groups_list_spec.js
index 91740c7ce3b..e7ac837a4c8 100644
--- a/spec/frontend/pages/projects/forks/new/components/fork_groups_list_spec.js
+++ b/spec/frontend/pages/projects/forks/new/components/fork_groups_list_spec.js
@@ -1,10 +1,10 @@
-import AxiosMockAdapter from 'axios-mock-adapter';
-import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import AxiosMockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
import ForkGroupsList from '~/pages/projects/forks/new/components/fork_groups_list.vue';
import ForkGroupsListItem from '~/pages/projects/forks/new/components/fork_groups_list_item.vue';
@@ -16,7 +16,6 @@ describe('Fork groups list component', () => {
const DEFAULT_PROPS = {
endpoint: '/dummy',
- hasReachedProjectLimit: false,
};
const replyWith = (...args) => axiosMock.onGet(DEFAULT_PROPS.endpoint).reply(...args);
@@ -94,10 +93,9 @@ describe('Fork groups list component', () => {
it('renders list items for each available group', async () => {
const namespaces = [{ name: 'dummy1' }, { name: 'dummy2' }, { name: 'otherdummy' }];
- const hasReachedProjectLimit = true;
replyWith(200, { namespaces });
- createWrapper({ hasReachedProjectLimit });
+ createWrapper();
await waitForPromises();
@@ -106,7 +104,6 @@ describe('Fork groups list component', () => {
namespaces.forEach((namespace, idx) => {
expect(wrapper.findAll(ForkGroupsListItem).at(idx).props()).toStrictEqual({
group: namespace,
- hasReachedProjectLimit,
});
});
});
diff --git a/spec/frontend/pages/projects/graphs/code_coverage_spec.js b/spec/frontend/pages/projects/graphs/code_coverage_spec.js
index 4a60c7fd509..1f9029b40c7 100644
--- a/spec/frontend/pages/projects/graphs/code_coverage_spec.js
+++ b/spec/frontend/pages/projects/graphs/code_coverage_spec.js
@@ -1,13 +1,13 @@
-import MockAdapter from 'axios-mock-adapter';
-import { shallowMount } from '@vue/test-utils';
import { GlAlert, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { GlAreaChart } from '@gitlab/ui/dist/charts';
+import { shallowMount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
+import httpStatusCodes from '~/lib/utils/http_status';
import CodeCoverage from '~/pages/projects/graphs/components/code_coverage.vue';
import { codeCoverageMockData, sortedDataByDates } from './mock_data';
-import httpStatusCodes from '~/lib/utils/http_status';
describe('Code Coverage', () => {
let wrapper;
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_a_spec.js.snap b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_a_spec.js.snap
new file mode 100644
index 00000000000..c9141d13a46
--- /dev/null
+++ b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_a_spec.js.snap
@@ -0,0 +1,66 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Learn GitLab Design A should render the loading state 1`] = `
+<ul>
+ <li>
+ <span>
+ Create a repository
+ </span>
+ </li>
+ <li>
+ <span>
+ Invite your colleagues
+ </span>
+ </li>
+ <li>
+ <span>
+ Set-up CI/CD
+ </span>
+ </li>
+ <li>
+ <span>
+ <gl-link-stub
+ href="http://example.com/"
+ >
+ Start a free trial of GitLab Gold
+ </gl-link-stub>
+ </span>
+ </li>
+ <li>
+ <span>
+ <gl-link-stub
+ href="http://example.com/"
+ >
+ Add code owners
+ </gl-link-stub>
+ </span>
+ </li>
+ <li>
+ <span>
+ <gl-link-stub
+ href="http://example.com/"
+ >
+ Enable require merge approvals
+ </gl-link-stub>
+ </span>
+ </li>
+ <li>
+ <span>
+ <gl-link-stub
+ href="http://example.com/"
+ >
+ Submit a merge request (MR)
+ </gl-link-stub>
+ </span>
+ </li>
+ <li>
+ <span>
+ <gl-link-stub
+ href="http://example.com/"
+ >
+ Run a Security scan using CI/CD
+ </gl-link-stub>
+ </span>
+ </li>
+</ul>
+`;
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_b_spec.js.snap b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_b_spec.js.snap
new file mode 100644
index 00000000000..85e3b675e5b
--- /dev/null
+++ b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_b_spec.js.snap
@@ -0,0 +1,66 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Learn GitLab Design B should render the loading state 1`] = `
+<ul>
+ <li>
+ <span>
+ Create a repository
+ </span>
+ </li>
+ <li>
+ <span>
+ Invite your colleagues
+ </span>
+ </li>
+ <li>
+ <span>
+ Set-up CI/CD
+ </span>
+ </li>
+ <li>
+ <span>
+ <gl-link-stub
+ href="http://example.com/"
+ >
+ Start a free trial of GitLab Gold
+ </gl-link-stub>
+ </span>
+ </li>
+ <li>
+ <span>
+ <gl-link-stub
+ href="http://example.com/"
+ >
+ Add code owners
+ </gl-link-stub>
+ </span>
+ </li>
+ <li>
+ <span>
+ <gl-link-stub
+ href="http://example.com/"
+ >
+ Enable require merge approvals
+ </gl-link-stub>
+ </span>
+ </li>
+ <li>
+ <span>
+ <gl-link-stub
+ href="http://example.com/"
+ >
+ Submit a merge request (MR)
+ </gl-link-stub>
+ </span>
+ </li>
+ <li>
+ <span>
+ <gl-link-stub
+ href="http://example.com/"
+ >
+ Run a Security scan using CI/CD
+ </gl-link-stub>
+ </span>
+ </li>
+</ul>
+`;
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_a_spec.js b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_a_spec.js
new file mode 100644
index 00000000000..ddc5339e7e0
--- /dev/null
+++ b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_a_spec.js
@@ -0,0 +1,63 @@
+import { shallowMount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import LearnGitlabA from '~/pages/projects/learn_gitlab/components/learn_gitlab_a.vue';
+
+const TEST_ACTIONS = {
+ gitWrite: {
+ url: 'http://example.com/',
+ completed: true,
+ },
+ userAdded: {
+ url: 'http://example.com/',
+ completed: true,
+ },
+ pipelineCreated: {
+ url: 'http://example.com/',
+ completed: true,
+ },
+ trialStarted: {
+ url: 'http://example.com/',
+ completed: false,
+ },
+ codeOwnersEnabled: {
+ url: 'http://example.com/',
+ completed: false,
+ },
+ requiredMrApprovalsEnabled: {
+ url: 'http://example.com/',
+ completed: false,
+ },
+ mergeRequestCreated: {
+ url: 'http://example.com/',
+ completed: false,
+ },
+ securityScanEnabled: {
+ url: 'http://example.com/',
+ completed: false,
+ },
+};
+
+describe('Learn GitLab Design A', () => {
+ let wrapper;
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const createWrapper = () => {
+ wrapper = extendedWrapper(
+ shallowMount(LearnGitlabA, {
+ propsData: {
+ actions: TEST_ACTIONS,
+ },
+ }),
+ );
+ };
+
+ it('should render the loading state', () => {
+ createWrapper();
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_b_spec.js b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_b_spec.js
new file mode 100644
index 00000000000..be4f5768402
--- /dev/null
+++ b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_b_spec.js
@@ -0,0 +1,63 @@
+import { shallowMount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import LearnGitlabA from '~/pages/projects/learn_gitlab/components/learn_gitlab_a.vue';
+
+const TEST_ACTIONS = {
+ gitWrite: {
+ url: 'http://example.com/',
+ completed: true,
+ },
+ userAdded: {
+ url: 'http://example.com/',
+ completed: true,
+ },
+ pipelineCreated: {
+ url: 'http://example.com/',
+ completed: true,
+ },
+ trialStarted: {
+ url: 'http://example.com/',
+ completed: false,
+ },
+ codeOwnersEnabled: {
+ url: 'http://example.com/',
+ completed: false,
+ },
+ requiredMrApprovalsEnabled: {
+ url: 'http://example.com/',
+ completed: false,
+ },
+ mergeRequestCreated: {
+ url: 'http://example.com/',
+ completed: false,
+ },
+ securityScanEnabled: {
+ url: 'http://example.com/',
+ completed: false,
+ },
+};
+
+describe('Learn GitLab Design B', () => {
+ let wrapper;
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const createWrapper = () => {
+ wrapper = extendedWrapper(
+ shallowMount(LearnGitlabA, {
+ propsData: {
+ actions: TEST_ACTIONS,
+ },
+ }),
+ );
+ };
+
+ it('should render the loading state', () => {
+ createWrapper();
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/pages/projects/merge_requests/edit/check_form_state_spec.js b/spec/frontend/pages/projects/merge_requests/edit/check_form_state_spec.js
new file mode 100644
index 00000000000..ea49111760b
--- /dev/null
+++ b/spec/frontend/pages/projects/merge_requests/edit/check_form_state_spec.js
@@ -0,0 +1,41 @@
+import initCheckFormState from '~/pages/projects/merge_requests/edit/check_form_state';
+
+describe('Check form state', () => {
+ const findInput = () => document.querySelector('#form-input');
+
+ let beforeUnloadEvent;
+ let setDialogContent;
+
+ beforeEach(() => {
+ setFixtures(`
+ <form class="merge-request-form">
+ <input type="text" name="test" id="form-input"/>
+ </form>`);
+
+ beforeUnloadEvent = new Event('beforeunload');
+ jest.spyOn(beforeUnloadEvent, 'preventDefault');
+ setDialogContent = jest.spyOn(beforeUnloadEvent, 'returnValue', 'set');
+
+ initCheckFormState();
+ });
+
+ afterEach(() => {
+ beforeUnloadEvent.preventDefault.mockRestore();
+ setDialogContent.mockRestore();
+ });
+
+ it('shows confirmation dialog when there are unsaved changes', () => {
+ findInput().value = 'value changed';
+ window.dispatchEvent(beforeUnloadEvent);
+
+ expect(beforeUnloadEvent.preventDefault).toHaveBeenCalled();
+ expect(setDialogContent).toHaveBeenCalledWith('');
+ });
+
+ it('does not show confirmation dialog when there are no unsaved changes', () => {
+ window.dispatchEvent(beforeUnloadEvent);
+
+ expect(beforeUnloadEvent.preventDefault).not.toHaveBeenCalled();
+ expect(setDialogContent).not.toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
index cfe54016410..5fed9fcaad2 100644
--- a/spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
+++ b/spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import Cookies from 'js-cookie';
import PipelineSchedulesCallout from '~/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue';
diff --git a/spec/frontend/pages/projects/shared/permissions/components/project_feature_settings_spec.js b/spec/frontend/pages/projects/shared/permissions/components/project_feature_settings_spec.js
index c90ebd47b08..0fffcf433a3 100644
--- a/spec/frontend/pages/projects/shared/permissions/components/project_feature_settings_spec.js
+++ b/spec/frontend/pages/projects/shared/permissions/components/project_feature_settings_spec.js
@@ -1,7 +1,6 @@
-import { mount, shallowMount } from '@vue/test-utils';
-
-import projectFeatureSetting from '~/pages/projects/shared/permissions/components/project_feature_setting.vue';
-import projectFeatureToggle from '~/vue_shared/components/toggle_button.vue';
+import { GlToggle } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import ProjectFeatureSetting from '~/pages/projects/shared/permissions/components/project_feature_setting.vue';
describe('Project Feature Settings', () => {
const defaultProps = {
@@ -19,81 +18,76 @@ describe('Project Feature Settings', () => {
};
let wrapper;
- const mountComponent = (customProps) => {
- const propsData = { ...defaultProps, ...customProps };
- return shallowMount(projectFeatureSetting, { propsData });
- };
+ const findHiddenInput = () => wrapper.find(`input[name=${defaultProps.name}]`);
+ const findToggle = () => wrapper.findComponent(GlToggle);
- beforeEach(() => {
- wrapper = mountComponent();
- });
+ const mountComponent = (customProps = {}) =>
+ shallowMount(ProjectFeatureSetting, {
+ propsData: {
+ ...defaultProps,
+ ...customProps,
+ },
+ });
afterEach(() => {
wrapper.destroy();
+ wrapper = null;
});
describe('Hidden name input', () => {
it('should set the hidden name input if the name exists', () => {
- expect(wrapper.find(`input[name=${defaultProps.name}]`).attributes().value).toBe('1');
+ wrapper = mountComponent();
+
+ expect(findHiddenInput().attributes('value')).toBe('1');
});
it('should not set the hidden name input if the name does not exist', () => {
- wrapper.setProps({ name: null });
+ wrapper = mountComponent({ name: null });
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find(`input[name=${defaultProps.name}]`).exists()).toBe(false);
- });
+ expect(findHiddenInput().exists()).toBe(false);
});
});
describe('Feature toggle', () => {
- it('should be hidden if "showToggle" is passed false', async () => {
- wrapper.setProps({ showToggle: false });
-
- await wrapper.vm.$nextTick();
+ it('should be hidden if "showToggle" is passed false', () => {
+ wrapper = mountComponent({ showToggle: false });
- expect(wrapper.find(projectFeatureToggle).element).toBeUndefined();
+ expect(findToggle().exists()).toBe(false);
});
it('should enable the feature toggle if the value is not 0', () => {
- expect(wrapper.find(projectFeatureToggle).props().value).toBe(true);
+ wrapper = mountComponent();
+
+ expect(findToggle().props('value')).toBe(true);
});
it('should enable the feature toggle if the value is less than 0', () => {
- wrapper.setProps({ value: -1 });
+ wrapper = mountComponent({ value: -1 });
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find(projectFeatureToggle).props().value).toBe(true);
- });
+ expect(findToggle().props('value')).toBe(true);
});
it('should disable the feature toggle if the value is 0', () => {
- wrapper.setProps({ value: 0 });
+ wrapper = mountComponent({ value: 0 });
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find(projectFeatureToggle).props().value).toBe(false);
- });
+ expect(findToggle().props('value')).toBe(false);
});
it('should disable the feature toggle if disabledInput is set', () => {
- wrapper.setProps({ disabledInput: true });
+ wrapper = mountComponent({ disabledInput: true });
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find(projectFeatureToggle).props().disabledInput).toBe(true);
- });
+ expect(findToggle().props('disabled')).toBe(true);
});
it('should emit a change event when the feature toggle changes', () => {
- // Needs to be fully mounted to be able to trigger the click event on the internal button
- wrapper = mount(projectFeatureSetting, { propsData: defaultProps });
+ wrapper = mountComponent({ propsData: defaultProps });
+
+ expect(wrapper.emitted('change')).toBeUndefined();
- expect(wrapper.emitted().change).toBeUndefined();
- wrapper.find(projectFeatureToggle).find('button').trigger('click');
+ findToggle().vm.$emit('change', false);
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted().change.length).toBe(1);
- expect(wrapper.emitted().change[0]).toEqual([0]);
- });
+ expect(wrapper.emitted('change')).toHaveLength(1);
+ expect(wrapper.emitted('change')[0]).toEqual([0]);
});
});
@@ -108,26 +102,23 @@ describe('Project Feature Settings', () => {
`(
'should set disabled to $isDisabled when disabledInput is $disabledInput, the value is $value and options are $options',
({ disabledInput, value, options, isDisabled }) => {
- wrapper.setProps({ disabledInput, value, options });
-
- return wrapper.vm.$nextTick(() => {
- if (isDisabled) {
- expect(wrapper.find('select').attributes().disabled).toEqual('disabled');
- } else {
- expect(wrapper.find('select').attributes().disabled).toBeUndefined();
- }
- });
+ wrapper = mountComponent({ disabledInput, value, options });
+
+ const expected = isDisabled ? 'disabled' : undefined;
+
+ expect(wrapper.find('select').attributes('disabled')).toBe(expected);
},
);
it('should emit the change when a new option is selected', () => {
- expect(wrapper.emitted().change).toBeUndefined();
+ wrapper = mountComponent();
+
+ expect(wrapper.emitted('change')).toBeUndefined();
+
wrapper.findAll('option').at(1).trigger('change');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted().change.length).toBe(1);
- expect(wrapper.emitted().change[0]).toEqual([2]);
- });
+ expect(wrapper.emitted('change')).toHaveLength(1);
+ expect(wrapper.emitted('change')[0]).toEqual([2]);
});
});
});
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 9aee6ec7ace..d7c754fd3cc 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
@@ -1,13 +1,12 @@
+import { GlToggle } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
-
+import projectFeatureSetting from '~/pages/projects/shared/permissions/components/project_feature_setting.vue';
import settingsPanel from '~/pages/projects/shared/permissions/components/settings_panel.vue';
import {
featureAccessLevel,
visibilityLevelDescriptions,
visibilityOptions,
} from '~/pages/projects/shared/permissions/constants';
-import projectFeatureSetting from '~/pages/projects/shared/permissions/components/project_feature_setting.vue';
-import projectFeatureToggle from '~/vue_shared/components/toggle_button.vue';
const defaultProps = {
currentSettings: {
@@ -69,57 +68,61 @@ describe('Settings Panel', () => {
});
};
- const overrideCurrentSettings = (
- currentSettingsProps,
- extraProps = {},
- mountFn = shallowMount,
- ) => {
- return mountComponent({ ...extraProps, currentSettings: currentSettingsProps }, mountFn);
- };
-
const findLFSSettingsRow = () => wrapper.find({ ref: 'git-lfs-settings' });
const findLFSSettingsMessage = () => findLFSSettingsRow().find('p');
- const findLFSFeatureToggle = () => findLFSSettingsRow().find(projectFeatureToggle);
-
+ const findLFSFeatureToggle = () => findLFSSettingsRow().find(GlToggle);
const findRepositoryFeatureProjectRow = () => wrapper.find({ ref: 'repository-settings' });
const findRepositoryFeatureSetting = () =>
findRepositoryFeatureProjectRow().find(projectFeatureSetting);
-
+ const findProjectVisibilitySettings = () => wrapper.find({ ref: 'project-visibility-settings' });
const findAnalyticsRow = () => wrapper.find({ ref: 'analytics-settings' });
-
- beforeEach(() => {
- wrapper = mountComponent();
- });
+ const findProjectVisibilityLevelInput = () => wrapper.find('[name="project[visibility_level]"]');
+ const findRequestAccessEnabledInput = () =>
+ wrapper.find('[name="project[request_access_enabled]"]');
+ const findMergeRequestsAccessLevelInput = () =>
+ wrapper.find('[name="project[project_feature_attributes][merge_requests_access_level]"]');
+ const findForkingAccessLevelInput = () =>
+ wrapper.find('[name="project[project_feature_attributes][forking_access_level]"]');
+ const findBuildsAccessLevelInput = () =>
+ wrapper.find('[name="project[project_feature_attributes][builds_access_level]"]');
+ const findContainerRegistrySettings = () => wrapper.find({ ref: 'container-registry-settings' });
+ const findContainerRegistryEnabledInput = () =>
+ wrapper.find('[name="project[container_registry_enabled]"]');
+ const findPackageSettings = () => wrapper.find({ ref: 'package-settings' });
+ const findPackagesEnabledInput = () => wrapper.find('[name="project[packages_enabled]"]');
+ const findPagesSettings = () => wrapper.find({ ref: 'pages-settings' });
+ const findEmailSettings = () => wrapper.find({ ref: 'email-settings' });
+ const findShowDefaultAwardEmojis = () =>
+ wrapper.find('input[name="project[project_setting_attributes][show_default_award_emojis]"]');
+ const findMetricsVisibilitySettings = () => wrapper.find({ ref: 'metrics-visibility-settings' });
+ const findAllowEditingCommitMessages = () =>
+ wrapper.find({ ref: 'allow-editing-commit-messages' }).exists();
+ const findOperationsSettings = () => wrapper.find({ ref: 'operations-settings' });
afterEach(() => {
wrapper.destroy();
+ wrapper = null;
});
describe('Project Visibility', () => {
it('should set the project visibility help path', () => {
- expect(wrapper.find({ ref: 'project-visibility-settings' }).props().helpPath).toBe(
+ wrapper = mountComponent();
+
+ expect(findProjectVisibilitySettings().props('helpPath')).toBe(
defaultProps.visibilityHelpPath,
);
});
it('should not disable the visibility level dropdown', () => {
- wrapper.setProps({ canChangeVisibilityLevel: true });
+ wrapper = mountComponent({ canChangeVisibilityLevel: true });
- return wrapper.vm.$nextTick(() => {
- expect(
- wrapper.find('[name="project[visibility_level]"]').attributes().disabled,
- ).toBeUndefined();
- });
+ expect(findProjectVisibilityLevelInput().attributes('disabled')).toBeUndefined();
});
it('should disable the visibility level dropdown', () => {
- wrapper.setProps({ canChangeVisibilityLevel: false });
+ wrapper = mountComponent({ canChangeVisibilityLevel: false });
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find('[name="project[visibility_level]"]').attributes().disabled).toBe(
- 'disabled',
- );
- });
+ expect(findProjectVisibilityLevelInput().attributes('disabled')).toBe('disabled');
});
it.each`
@@ -133,232 +136,209 @@ describe('Settings Panel', () => {
`(
'sets disabled to $disabled for the visibility option $option when given $allowedOptions',
({ option, allowedOptions, disabled }) => {
- wrapper.setProps({ allowedVisibilityOptions: allowedOptions });
-
- return wrapper.vm.$nextTick(() => {
- const attributeValue = wrapper
- .find(`[name="project[visibility_level]"] option[value="${option}"]`)
- .attributes().disabled;
-
- if (disabled) {
- expect(attributeValue).toBe('disabled');
- } else {
- expect(attributeValue).toBeUndefined();
- }
- });
+ wrapper = mountComponent({ allowedVisibilityOptions: allowedOptions });
+
+ const attributeValue = findProjectVisibilityLevelInput()
+ .find(`option[value="${option}"]`)
+ .attributes('disabled');
+
+ const expected = disabled ? 'disabled' : undefined;
+
+ expect(attributeValue).toBe(expected);
},
);
it('should set the visibility level description based upon the selected visibility level', () => {
- wrapper.find('[name="project[visibility_level]"]').setValue(visibilityOptions.INTERNAL);
+ wrapper = mountComponent();
+
+ findProjectVisibilityLevelInput().setValue(visibilityOptions.INTERNAL);
- expect(wrapper.find({ ref: 'project-visibility-settings' }).text()).toContain(
+ expect(findProjectVisibilitySettings().text()).toContain(
visibilityLevelDescriptions[visibilityOptions.INTERNAL],
);
});
it('should show the request access checkbox if the visibility level is not private', () => {
- wrapper = overrideCurrentSettings({ visibilityLevel: visibilityOptions.INTERNAL });
+ wrapper = mountComponent({
+ currentSettings: { visibilityLevel: visibilityOptions.INTERNAL },
+ });
- expect(wrapper.find('[name="project[request_access_enabled]"]').exists()).toBe(true);
+ expect(findRequestAccessEnabledInput().exists()).toBe(true);
});
it('should not show the request access checkbox if the visibility level is private', () => {
- wrapper = overrideCurrentSettings({ visibilityLevel: visibilityOptions.PRIVATE });
+ wrapper = mountComponent({ currentSettings: { visibilityLevel: visibilityOptions.PRIVATE } });
- expect(wrapper.find('[name="project[request_access_enabled]"]').exists()).toBe(false);
+ expect(findRequestAccessEnabledInput().exists()).toBe(false);
});
});
describe('Repository', () => {
it('should set the repository help text when the visibility level is set to private', () => {
- wrapper = overrideCurrentSettings({ visibilityLevel: visibilityOptions.PRIVATE });
+ wrapper = mountComponent({ currentSettings: { visibilityLevel: visibilityOptions.PRIVATE } });
- expect(findRepositoryFeatureProjectRow().props().helpText).toBe(
- 'View and edit files in this project',
+ expect(findRepositoryFeatureProjectRow().props('helpText')).toBe(
+ 'View and edit files in this project.',
);
});
it('should set the repository help text with a read access warning when the visibility level is set to non-private', () => {
- wrapper = overrideCurrentSettings({ visibilityLevel: visibilityOptions.PUBLIC });
+ wrapper = mountComponent({ currentSettings: { visibilityLevel: visibilityOptions.PUBLIC } });
- expect(findRepositoryFeatureProjectRow().props().helpText).toBe(
- 'View and edit files in this project. Non-project members will only have read access',
+ expect(findRepositoryFeatureProjectRow().props('helpText')).toBe(
+ 'View and edit files in this project. Non-project members will only have read access.',
);
});
});
describe('Merge requests', () => {
it('should enable the merge requests access level input when the repository is enabled', () => {
- wrapper = overrideCurrentSettings({ repositoryAccessLevel: featureAccessLevel.EVERYONE });
+ wrapper = mountComponent({
+ currentSettings: { repositoryAccessLevel: featureAccessLevel.EVERYONE },
+ });
- expect(
- wrapper
- .find('[name="project[project_feature_attributes][merge_requests_access_level]"]')
- .props().disabledInput,
- ).toBe(false);
+ expect(findMergeRequestsAccessLevelInput().props('disabledInput')).toBe(false);
});
it('should disable the merge requests access level input when the repository is disabled', () => {
- wrapper = overrideCurrentSettings({ repositoryAccessLevel: featureAccessLevel.NOT_ENABLED });
+ wrapper = mountComponent({
+ currentSettings: { repositoryAccessLevel: featureAccessLevel.NOT_ENABLED },
+ });
- expect(
- wrapper
- .find('[name="project[project_feature_attributes][merge_requests_access_level]"]')
- .props().disabledInput,
- ).toBe(true);
+ expect(findMergeRequestsAccessLevelInput().props('disabledInput')).toBe(true);
});
});
describe('Forks', () => {
it('should enable the forking access level input when the repository is enabled', () => {
- wrapper = overrideCurrentSettings({ repositoryAccessLevel: featureAccessLevel.EVERYONE });
+ wrapper = mountComponent({
+ currentSettings: { repositoryAccessLevel: featureAccessLevel.EVERYONE },
+ });
- expect(
- wrapper.find('[name="project[project_feature_attributes][forking_access_level]"]').props()
- .disabledInput,
- ).toBe(false);
+ expect(findForkingAccessLevelInput().props('disabledInput')).toBe(false);
});
it('should disable the forking access level input when the repository is disabled', () => {
- wrapper = overrideCurrentSettings({ repositoryAccessLevel: featureAccessLevel.NOT_ENABLED });
+ wrapper = mountComponent({
+ currentSettings: { repositoryAccessLevel: featureAccessLevel.NOT_ENABLED },
+ });
- expect(
- wrapper.find('[name="project[project_feature_attributes][forking_access_level]"]').props()
- .disabledInput,
- ).toBe(true);
+ expect(findForkingAccessLevelInput().props('disabledInput')).toBe(true);
});
});
describe('Pipelines', () => {
it('should enable the builds access level input when the repository is enabled', () => {
- wrapper = overrideCurrentSettings({ repositoryAccessLevel: featureAccessLevel.EVERYONE });
+ wrapper = mountComponent({
+ currentSettings: { repositoryAccessLevel: featureAccessLevel.EVERYONE },
+ });
- expect(
- wrapper.find('[name="project[project_feature_attributes][builds_access_level]"]').props()
- .disabledInput,
- ).toBe(false);
+ expect(findBuildsAccessLevelInput().props('disabledInput')).toBe(false);
});
it('should disable the builds access level input when the repository is disabled', () => {
- wrapper = overrideCurrentSettings({ repositoryAccessLevel: featureAccessLevel.NOT_ENABLED });
+ wrapper = mountComponent({
+ currentSettings: { repositoryAccessLevel: featureAccessLevel.NOT_ENABLED },
+ });
- expect(
- wrapper.find('[name="project[project_feature_attributes][builds_access_level]"]').props()
- .disabledInput,
- ).toBe(true);
+ expect(findBuildsAccessLevelInput().props('disabledInput')).toBe(true);
});
});
describe('Container registry', () => {
it('should show the container registry settings if the registry is available', () => {
- wrapper.setProps({ registryAvailable: true });
+ wrapper = mountComponent({ registryAvailable: true });
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find({ ref: 'container-registry-settings' }).exists()).toBe(true);
- });
+ expect(findContainerRegistrySettings().exists()).toBe(true);
});
it('should hide the container registry settings if the registry is not available', () => {
- wrapper.setProps({ registryAvailable: false });
+ wrapper = mountComponent({ registryAvailable: false });
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find({ ref: 'container-registry-settings' }).exists()).toBe(false);
- });
+ expect(findContainerRegistrySettings().exists()).toBe(false);
});
it('should set the container registry settings help path', () => {
- wrapper.setProps({ registryAvailable: true });
+ wrapper = mountComponent({ registryAvailable: true });
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find({ ref: 'container-registry-settings' }).props().helpPath).toBe(
- defaultProps.registryHelpPath,
- );
- });
+ expect(findContainerRegistrySettings().props('helpPath')).toBe(defaultProps.registryHelpPath);
});
it('should show the container registry public note if the visibility level is public and the registry is available', () => {
- wrapper = overrideCurrentSettings(
- { visibilityLevel: visibilityOptions.PUBLIC },
- { registryAvailable: true },
- );
+ wrapper = mountComponent({
+ currentSettings: { visibilityLevel: visibilityOptions.PUBLIC },
+ registryAvailable: true,
+ });
- expect(wrapper.find({ ref: 'container-registry-settings' }).text()).toContain(
+ expect(findContainerRegistrySettings().text()).toContain(
'Note: the container registry is always visible when a project is public',
);
});
it('should hide the container registry public note if the visibility level is private and the registry is available', () => {
- wrapper = overrideCurrentSettings(
- { visibilityLevel: visibilityOptions.PRIVATE },
- { registryAvailable: true },
- );
+ wrapper = mountComponent({
+ currentSettings: { visibilityLevel: visibilityOptions.PRIVATE },
+ registryAvailable: true,
+ });
- expect(wrapper.find({ ref: 'container-registry-settings' }).text()).not.toContain(
+ expect(findContainerRegistrySettings().text()).not.toContain(
'Note: the container registry is always visible when a project is public',
);
});
it('should enable the container registry input when the repository is enabled', () => {
- wrapper = overrideCurrentSettings(
- { repositoryAccessLevel: featureAccessLevel.EVERYONE },
- { registryAvailable: true },
- );
+ wrapper = mountComponent({
+ currentSettings: { repositoryAccessLevel: featureAccessLevel.EVERYONE },
+ registryAvailable: true,
+ });
- expect(
- wrapper.find('[name="project[container_registry_enabled]"]').props().disabledInput,
- ).toBe(false);
+ expect(findContainerRegistryEnabledInput().props('disabled')).toBe(false);
});
it('should disable the container registry input when the repository is disabled', () => {
- wrapper = overrideCurrentSettings(
- { repositoryAccessLevel: featureAccessLevel.NOT_ENABLED },
- { registryAvailable: true },
- );
+ wrapper = mountComponent({
+ currentSettings: { repositoryAccessLevel: featureAccessLevel.NOT_ENABLED },
+ registryAvailable: true,
+ });
- expect(
- wrapper.find('[name="project[container_registry_enabled]"]').props().disabledInput,
- ).toBe(true);
+ expect(findContainerRegistryEnabledInput().props('disabled')).toBe(true);
});
});
describe('Git Large File Storage', () => {
it('should show the LFS settings if LFS is available', () => {
- wrapper.setProps({ lfsAvailable: true });
+ wrapper = mountComponent({ lfsAvailable: true });
- return wrapper.vm.$nextTick(() => {
- expect(findLFSSettingsRow().exists()).toBe(true);
- });
+ expect(findLFSSettingsRow().exists()).toBe(true);
});
it('should hide the LFS settings if LFS is not available', () => {
- wrapper.setProps({ lfsAvailable: false });
+ wrapper = mountComponent({ lfsAvailable: false });
- return wrapper.vm.$nextTick(() => {
- expect(findLFSSettingsRow().exists()).toBe(false);
- });
+ expect(findLFSSettingsRow().exists()).toBe(false);
});
it('should set the LFS settings help path', () => {
- expect(findLFSSettingsRow().props().helpPath).toBe(defaultProps.lfsHelpPath);
+ wrapper = mountComponent();
+ expect(findLFSSettingsRow().props('helpPath')).toBe(defaultProps.lfsHelpPath);
});
it('should enable the LFS input when the repository is enabled', () => {
- wrapper = overrideCurrentSettings(
- { repositoryAccessLevel: featureAccessLevel.EVERYONE },
- { lfsAvailable: true },
- );
+ wrapper = mountComponent({
+ currentSettings: { repositoryAccessLevel: featureAccessLevel.EVERYONE },
+ lfsAvailable: true,
+ });
- expect(findLFSFeatureToggle().props().disabledInput).toBe(false);
+ expect(findLFSFeatureToggle().props('disabled')).toBe(false);
});
it('should disable the LFS input when the repository is disabled', () => {
- wrapper = overrideCurrentSettings(
- { repositoryAccessLevel: featureAccessLevel.NOT_ENABLED },
- { lfsAvailable: true },
- );
+ wrapper = mountComponent({
+ currentSettings: { repositoryAccessLevel: featureAccessLevel.NOT_ENABLED },
+ lfsAvailable: true,
+ });
- expect(findLFSFeatureToggle().props().disabledInput).toBe(true);
+ expect(findLFSFeatureToggle().props('disabled')).toBe(true);
});
it('should not change lfsEnabled when disabling the repository', async () => {
@@ -373,8 +353,7 @@ describe('Settings Panel', () => {
expect(isToggleButtonChecked(lfsFeatureToggleButton)).toBe(true);
expect(isToggleButtonChecked(repositoryFeatureToggleButton)).toBe(true);
- repositoryFeatureToggleButton.trigger('click');
- await wrapper.vm.$nextTick();
+ await repositoryFeatureToggleButton.trigger('click');
expect(isToggleButtonChecked(repositoryFeatureToggleButton)).toBe(false);
// LFS toggle should still be checked
@@ -400,7 +379,7 @@ describe('Settings Panel', () => {
const link = message.find('a');
expect(message.text()).toContain(
- 'LFS objects from this repository are still available to forks',
+ 'LFS objects from this repository are available to forks.',
);
expect(link.text()).toBe('How do I remove them?');
expect(link.attributes('href')).toBe(
@@ -418,47 +397,39 @@ describe('Settings Panel', () => {
describe('Packages', () => {
it('should show the packages settings if packages are available', () => {
- wrapper.setProps({ packagesAvailable: true });
+ wrapper = mountComponent({ packagesAvailable: true });
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find({ ref: 'package-settings' }).exists()).toBe(true);
- });
+ expect(findPackageSettings().exists()).toBe(true);
});
it('should hide the packages settings if packages are not available', () => {
- wrapper.setProps({ packagesAvailable: false });
+ wrapper = mountComponent({ packagesAvailable: false });
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find({ ref: 'package-settings' }).exists()).toBe(false);
- });
+ expect(findPackageSettings().exists()).toBe(false);
});
it('should set the package settings help path', () => {
- wrapper.setProps({ packagesAvailable: true });
+ wrapper = mountComponent({ packagesAvailable: true });
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find({ ref: 'package-settings' }).props().helpPath).toBe(
- defaultProps.packagesHelpPath,
- );
- });
+ expect(findPackageSettings().props('helpPath')).toBe(defaultProps.packagesHelpPath);
});
it('should enable the packages input when the repository is enabled', () => {
- wrapper = overrideCurrentSettings(
- { repositoryAccessLevel: featureAccessLevel.EVERYONE },
- { packagesAvailable: true },
- );
+ wrapper = mountComponent({
+ currentSettings: { repositoryAccessLevel: featureAccessLevel.EVERYONE },
+ packagesAvailable: true,
+ });
- expect(wrapper.find('[name="project[packages_enabled]"]').props().disabledInput).toBe(false);
+ expect(findPackagesEnabledInput().props('disabled')).toBe(false);
});
it('should disable the packages input when the repository is disabled', () => {
- wrapper = overrideCurrentSettings(
- { repositoryAccessLevel: featureAccessLevel.NOT_ENABLED },
- { packagesAvailable: true },
- );
+ wrapper = mountComponent({
+ currentSettings: { repositoryAccessLevel: featureAccessLevel.NOT_ENABLED },
+ packagesAvailable: true,
+ });
- expect(wrapper.find('[name="project[packages_enabled]"]').props().disabledInput).toBe(true);
+ expect(findPackagesEnabledInput().props('disabled')).toBe(true);
});
});
@@ -472,78 +443,66 @@ describe('Settings Panel', () => {
`(
'should $visibility the page settings if pagesAvailable is $pagesAvailable and pagesAccessControlEnabled is $pagesAccessControlEnabled',
({ pagesAvailable, pagesAccessControlEnabled, visibility }) => {
- wrapper.setProps({ pagesAvailable, pagesAccessControlEnabled });
+ wrapper = mountComponent({ pagesAvailable, pagesAccessControlEnabled });
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find({ ref: 'pages-settings' }).exists()).toBe(visibility === 'show');
- });
+ expect(findPagesSettings().exists()).toBe(visibility === 'show');
},
);
it('should set the pages settings help path', () => {
- wrapper.setProps({ pagesAvailable: true, pagesAccessControlEnabled: true });
+ wrapper = mountComponent({ pagesAvailable: true, pagesAccessControlEnabled: true });
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find({ ref: 'pages-settings' }).props().helpPath).toBe(
- defaultProps.pagesHelpPath,
- );
- });
+ expect(findPagesSettings().props('helpPath')).toBe(defaultProps.pagesHelpPath);
});
});
describe('Email notifications', () => {
it('should show the disable email notifications input if emails an be disabled', () => {
- wrapper.setProps({ canDisableEmails: true });
+ wrapper = mountComponent({ canDisableEmails: true });
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find({ ref: 'email-settings' }).exists()).toBe(true);
- });
+ expect(findEmailSettings().exists()).toBe(true);
});
it('should hide the disable email notifications input if emails cannot be disabled', () => {
- wrapper.setProps({ canDisableEmails: false });
+ wrapper = mountComponent({ canDisableEmails: false });
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find({ ref: 'email-settings' }).exists()).toBe(false);
- });
+ expect(findEmailSettings().exists()).toBe(false);
});
});
describe('Default award emojis', () => {
it('should show the "Show default award emojis" input', () => {
- return wrapper.vm.$nextTick(() => {
- expect(
- wrapper
- .find('input[name="project[project_setting_attributes][show_default_award_emojis]"]')
- .exists(),
- ).toBe(true);
- });
+ wrapper = mountComponent();
+
+ expect(findShowDefaultAwardEmojis().exists()).toBe(true);
});
});
describe('Metrics dashboard', () => {
it('should show the metrics dashboard access toggle', () => {
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find({ ref: 'metrics-visibility-settings' }).exists()).toBe(true);
- });
+ wrapper = mountComponent();
+
+ expect(findMetricsVisibilitySettings().exists()).toBe(true);
});
it('should contain help text', () => {
- expect(wrapper.find({ ref: 'metrics-visibility-settings' }).props().helpText).toBe(
- 'With Metrics Dashboard you can visualize this project performance metrics',
+ wrapper = mountComponent();
+
+ expect(findMetricsVisibilitySettings().props('helpText')).toBe(
+ "Visualize the project's performance metrics.",
);
});
it.each`
- scenario | selectedOption | selectedOptionLabel
- ${{ visibilityLevel: visibilityOptions.PRIVATE }} | ${String(featureAccessLevel.PROJECT_MEMBERS)} | ${'Only Project Members'}
- ${{ operationsAccessLevel: featureAccessLevel.NOT_ENABLED }} | ${String(featureAccessLevel.NOT_ENABLED)} | ${'Enable feature to choose access level'}
+ scenario | selectedOption | selectedOptionLabel
+ ${{ currentSettings: { visibilityLevel: visibilityOptions.PRIVATE } }} | ${String(featureAccessLevel.PROJECT_MEMBERS)} | ${'Only Project Members'}
+ ${{ currentSettings: { operationsAccessLevel: featureAccessLevel.NOT_ENABLED } }} | ${String(featureAccessLevel.NOT_ENABLED)} | ${'Enable feature to choose access level'}
`(
'should disable the metrics visibility dropdown when #scenario',
({ scenario, selectedOption, selectedOptionLabel }) => {
- wrapper = overrideCurrentSettings(scenario, {}, mount);
+ wrapper = mountComponent(scenario, mount);
- const select = wrapper.find({ ref: 'metrics-visibility-settings' }).find('select');
+ const select = findMetricsVisibilitySettings().find('select');
const option = select.find('option');
expect(select.attributes('disabled')).toBe('disabled');
@@ -556,31 +515,29 @@ describe('Settings Panel', () => {
describe('Settings panel with feature flags', () => {
describe('Allow edit of commit message', () => {
- it('should show the allow editing of commit messages checkbox', async () => {
+ it('should show the allow editing of commit messages checkbox', () => {
wrapper = mountComponent({
glFeatures: { allowEditingCommitMessages: true },
});
- await wrapper.vm.$nextTick();
-
- expect(wrapper.find({ ref: 'allow-editing-commit-messages' }).exists()).toBe(true);
+ expect(findAllowEditingCommitMessages()).toBe(true);
});
});
});
describe('Analytics', () => {
- it('should show the analytics toggle', async () => {
- await wrapper.vm.$nextTick();
+ it('should show the analytics toggle', () => {
+ wrapper = mountComponent();
expect(findAnalyticsRow().exists()).toBe(true);
});
});
describe('Operations', () => {
- it('should show the operations toggle', async () => {
- await wrapper.vm.$nextTick();
+ it('should show the operations toggle', () => {
+ wrapper = mountComponent();
- expect(wrapper.find({ ref: 'operations-settings' }).exists()).toBe(true);
+ expect(findOperationsSettings().exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/performance_bar/components/detailed_metric_spec.js b/spec/frontend/performance_bar/components/detailed_metric_spec.js
index 754ffcc12e3..6ddd047d549 100644
--- a/spec/frontend/performance_bar/components/detailed_metric_spec.js
+++ b/spec/frontend/performance_bar/components/detailed_metric_spec.js
@@ -1,5 +1,5 @@
-import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { trimText } from 'helpers/text_helper';
import DetailedMetric from '~/performance_bar/components/detailed_metric.vue';
import RequestWarning from '~/performance_bar/components/request_warning.vue';
diff --git a/spec/frontend/persistent_user_callout_spec.js b/spec/frontend/persistent_user_callout_spec.js
index 578fd8d836a..403142d7ff7 100644
--- a/spec/frontend/persistent_user_callout_spec.js
+++ b/spec/frontend/persistent_user_callout_spec.js
@@ -1,8 +1,8 @@
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
+import { deprecatedCreateFlash as Flash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import PersistentUserCallout from '~/persistent_user_callout';
-import { deprecatedCreateFlash as Flash } from '~/flash';
jest.mock('~/flash');
diff --git a/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js b/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js
index aae25a3aa6d..5dae77a4626 100644
--- a/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js
+++ b/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js
@@ -1,11 +1,11 @@
-import { shallowMount, mount } from '@vue/test-utils';
import { GlFormInput, GlFormTextarea } from '@gitlab/ui';
+import { shallowMount, mount } from '@vue/test-utils';
import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
import { mockCommitMessage, mockDefaultBranch } from '../../mock_data';
-describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
+describe('Pipeline Editor | Commit Form', () => {
let wrapper;
const createComponent = ({ props = {} } = {}, mountFn = shallowMount) => {
@@ -21,8 +21,8 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
});
};
- const findCommitTextarea = () => wrapper.find(GlFormTextarea);
- const findBranchInput = () => wrapper.find(GlFormInput);
+ const findCommitTextarea = () => wrapper.findComponent(GlFormTextarea);
+ const findBranchInput = () => wrapper.findComponent(GlFormInput);
const findNewMrCheckbox = () => wrapper.find('[data-testid="new-mr-checkbox"]');
const findSubmitBtn = () => wrapper.find('[type="submit"]');
const findCancelBtn = () => wrapper.find('[type="reset"]');
diff --git a/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js b/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js
new file mode 100644
index 00000000000..b87ff6ec0de
--- /dev/null
+++ b/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js
@@ -0,0 +1,223 @@
+import { GlFormTextarea, GlFormInput, GlLoadingIcon } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { objectToQuery, redirectTo } from '~/lib/utils/url_utility';
+import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
+import CommitSection from '~/pipeline_editor/components/commit/commit_section.vue';
+import { COMMIT_SUCCESS } from '~/pipeline_editor/constants';
+import commitCreate from '~/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql';
+
+import {
+ mockCiConfigPath,
+ mockCiYml,
+ mockCommitSha,
+ mockCommitNextSha,
+ mockCommitMessage,
+ mockDefaultBranch,
+ mockProjectFullPath,
+ mockNewMergeRequestPath,
+} from '../../mock_data';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ redirectTo: jest.fn(),
+ refreshCurrentPage: jest.fn(),
+ objectToQuery: jest.requireActual('~/lib/utils/url_utility').objectToQuery,
+ mergeUrlParams: jest.requireActual('~/lib/utils/url_utility').mergeUrlParams,
+}));
+
+const mockVariables = {
+ projectPath: mockProjectFullPath,
+ startBranch: mockDefaultBranch,
+ message: mockCommitMessage,
+ filePath: mockCiConfigPath,
+ content: mockCiYml,
+ lastCommitId: mockCommitSha,
+};
+
+const mockProvide = {
+ ciConfigPath: mockCiConfigPath,
+ defaultBranch: mockDefaultBranch,
+ projectFullPath: mockProjectFullPath,
+ newMergeRequestPath: mockNewMergeRequestPath,
+};
+
+describe('Pipeline Editor | Commit section', () => {
+ let wrapper;
+ let mockMutate;
+
+ const defaultProps = { ciFileContent: mockCiYml };
+
+ const createComponent = ({ props = {}, options = {}, provide = {} } = {}) => {
+ mockMutate = jest.fn().mockResolvedValue({
+ data: {
+ commitCreate: {
+ errors: [],
+ commit: {
+ sha: mockCommitNextSha,
+ },
+ },
+ },
+ });
+
+ wrapper = mount(CommitSection, {
+ propsData: { ...defaultProps, ...props },
+ provide: { ...mockProvide, ...provide },
+ data() {
+ return {
+ commitSha: mockCommitSha,
+ };
+ },
+ mocks: {
+ $apollo: {
+ mutate: mockMutate,
+ },
+ },
+ attachTo: document.body,
+ ...options,
+ });
+ };
+
+ const findCommitForm = () => wrapper.findComponent(CommitForm);
+ const findCommitBtnLoadingIcon = () =>
+ wrapper.find('[type="submit"]').findComponent(GlLoadingIcon);
+
+ const submitCommit = async ({
+ message = mockCommitMessage,
+ branch = mockDefaultBranch,
+ openMergeRequest = false,
+ } = {}) => {
+ await findCommitForm().findComponent(GlFormTextarea).setValue(message);
+ await findCommitForm().findComponent(GlFormInput).setValue(branch);
+ if (openMergeRequest) {
+ await findCommitForm().find('[data-testid="new-mr-checkbox"]').setChecked(openMergeRequest);
+ }
+ await findCommitForm().find('[type="submit"]').trigger('click');
+ // Simulate the write to local cache that occurs after a commit
+ await wrapper.setData({ commitSha: mockCommitNextSha });
+ };
+
+ const cancelCommitForm = async () => {
+ const findCancelBtn = () => wrapper.find('[type="reset"]');
+ await findCancelBtn().trigger('click');
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ mockMutate.mockReset();
+
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('when the user commits changes to the current branch', () => {
+ beforeEach(async () => {
+ await submitCommit();
+ });
+
+ it('calls the mutation with the default branch', () => {
+ expect(mockMutate).toHaveBeenCalledTimes(1);
+ expect(mockMutate).toHaveBeenCalledWith({
+ mutation: commitCreate,
+ update: expect.any(Function),
+ variables: {
+ ...mockVariables,
+ branch: mockDefaultBranch,
+ },
+ });
+ });
+
+ it('emits an event to communicate the commit was successful', () => {
+ expect(wrapper.emitted('commit')).toHaveLength(1);
+ expect(wrapper.emitted('commit')[0]).toEqual([{ type: COMMIT_SUCCESS }]);
+ });
+
+ it('shows no saving state', () => {
+ expect(findCommitBtnLoadingIcon().exists()).toBe(false);
+ });
+
+ it('a second commit submits the latest sha, keeping the form updated', async () => {
+ await submitCommit();
+
+ expect(mockMutate).toHaveBeenCalledTimes(2);
+ expect(mockMutate).toHaveBeenCalledWith({
+ mutation: commitCreate,
+ update: expect.any(Function),
+ variables: {
+ ...mockVariables,
+ lastCommitId: mockCommitNextSha,
+ branch: mockDefaultBranch,
+ },
+ });
+ });
+ });
+
+ describe('when the user commits changes to a new branch', () => {
+ const newBranch = 'new-branch';
+
+ beforeEach(async () => {
+ await submitCommit({
+ branch: newBranch,
+ });
+ });
+
+ it('calls the mutation with the new branch', () => {
+ expect(mockMutate).toHaveBeenCalledWith({
+ mutation: commitCreate,
+ update: expect.any(Function),
+ variables: {
+ ...mockVariables,
+ branch: newBranch,
+ },
+ });
+ });
+ });
+
+ describe('when the user commits changes to open a new merge request', () => {
+ const newBranch = 'new-branch';
+
+ beforeEach(async () => {
+ await submitCommit({
+ branch: newBranch,
+ openMergeRequest: true,
+ });
+ });
+
+ it('redirects to the merge request page with source and target branches', () => {
+ const branchesQuery = objectToQuery({
+ 'merge_request[source_branch]': newBranch,
+ 'merge_request[target_branch]': mockDefaultBranch,
+ });
+
+ expect(redirectTo).toHaveBeenCalledWith(`${mockNewMergeRequestPath}?${branchesQuery}`);
+ });
+ });
+
+ describe('when the commit is ocurring', () => {
+ it('shows a saving state', async () => {
+ mockMutate.mockImplementationOnce(() => {
+ expect(findCommitBtnLoadingIcon().exists()).toBe(true);
+ return Promise.resolve();
+ });
+
+ await submitCommit({
+ message: mockCommitMessage,
+ branch: mockDefaultBranch,
+ openMergeRequest: false,
+ });
+ });
+ });
+
+ describe('when the commit form is cancelled', () => {
+ beforeEach(async () => {
+ createComponent();
+ });
+
+ it('emits an event so that it cab be reseted', async () => {
+ await cancelCommitForm();
+
+ expect(wrapper.emitted('resetContent')).toHaveLength(1);
+ });
+ });
+});
diff --git a/spec/frontend/pipeline_editor/components/editor/ci_config_merged_preview_spec.js b/spec/frontend/pipeline_editor/components/editor/ci_config_merged_preview_spec.js
new file mode 100644
index 00000000000..866069f337b
--- /dev/null
+++ b/spec/frontend/pipeline_editor/components/editor/ci_config_merged_preview_spec.js
@@ -0,0 +1,88 @@
+import { GlAlert, GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+
+import { EDITOR_READY_EVENT } from '~/editor/constants';
+import CiConfigMergedPreview from '~/pipeline_editor/components/editor/ci_config_merged_preview.vue';
+import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants';
+import { INVALID_CI_CONFIG } from '~/pipelines/constants';
+import { mockLintResponse, mockCiConfigPath } from '../../mock_data';
+
+describe('Text editor component', () => {
+ let wrapper;
+
+ const MockEditorLite = {
+ template: '<div/>',
+ props: ['value', 'fileName', 'editorOptions'],
+ mounted() {
+ this.$emit(EDITOR_READY_EVENT);
+ },
+ };
+
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = shallowMount(CiConfigMergedPreview, {
+ propsData: {
+ ciConfigData: mockLintResponse,
+ ...props,
+ },
+ provide: {
+ ciConfigPath: mockCiConfigPath,
+ },
+ stubs: {
+ EditorLite: MockEditorLite,
+ },
+ });
+ };
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findIcon = () => wrapper.findComponent(GlIcon);
+ const findEditor = () => wrapper.findComponent(MockEditorLite);
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('when status is invalid', () => {
+ beforeEach(() => {
+ createComponent({ props: { ciConfigData: { status: CI_CONFIG_STATUS_INVALID } } });
+ });
+
+ it('show an error message', () => {
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(wrapper.vm.$options.errorTexts[INVALID_CI_CONFIG]);
+ });
+
+ it('hides the editor', () => {
+ expect(findEditor().exists()).toBe(false);
+ });
+ });
+
+ describe('when status is valid', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('shows an information message that the section is not editable', () => {
+ expect(findIcon().exists()).toBe(true);
+ expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.viewOnlyMessage);
+ });
+
+ it('contains an editor', () => {
+ expect(findEditor().exists()).toBe(true);
+ });
+
+ it('editor contains the value provided', () => {
+ expect(findEditor().props('value')).toBe(mockLintResponse.mergedYaml);
+ });
+
+ it('editor is configured for the CI config path', () => {
+ expect(findEditor().props('fileName')).toBe(mockCiConfigPath);
+ });
+
+ it('editor is readonly', () => {
+ expect(findEditor().props('editorOptions')).toMatchObject({
+ readOnly: true,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js b/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js
new file mode 100644
index 00000000000..3bf5a291c69
--- /dev/null
+++ b/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js
@@ -0,0 +1,120 @@
+import { shallowMount } from '@vue/test-utils';
+
+import { EDITOR_READY_EVENT } from '~/editor/constants';
+import TextEditor from '~/pipeline_editor/components/editor/text_editor.vue';
+import {
+ mockCiConfigPath,
+ mockCiYml,
+ mockCommitSha,
+ mockProjectPath,
+ mockProjectNamespace,
+} from '../../mock_data';
+
+describe('Pipeline Editor | Text editor component', () => {
+ let wrapper;
+
+ let editorReadyListener;
+ let mockUse;
+ let mockRegisterCiSchema;
+
+ const MockEditorLite = {
+ template: '<div/>',
+ props: ['value', 'fileName'],
+ mounted() {
+ this.$emit(EDITOR_READY_EVENT);
+ },
+ methods: {
+ getEditor: () => ({
+ use: mockUse,
+ registerCiSchema: mockRegisterCiSchema,
+ }),
+ },
+ };
+
+ const createComponent = (opts = {}, mountFn = shallowMount) => {
+ wrapper = mountFn(TextEditor, {
+ provide: {
+ projectPath: mockProjectPath,
+ projectNamespace: mockProjectNamespace,
+ ciConfigPath: mockCiConfigPath,
+ },
+ attrs: {
+ value: mockCiYml,
+ },
+ // Simulate graphQL client query result
+ data() {
+ return {
+ commitSha: mockCommitSha,
+ };
+ },
+ listeners: {
+ [EDITOR_READY_EVENT]: editorReadyListener,
+ },
+ stubs: {
+ EditorLite: MockEditorLite,
+ },
+ ...opts,
+ });
+ };
+
+ const findEditor = () => wrapper.findComponent(MockEditorLite);
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+
+ mockUse.mockClear();
+ mockRegisterCiSchema.mockClear();
+ });
+
+ describe('template', () => {
+ beforeEach(() => {
+ editorReadyListener = jest.fn();
+ mockUse = jest.fn();
+ mockRegisterCiSchema = jest.fn();
+
+ createComponent();
+ });
+
+ it('contains an editor', () => {
+ expect(findEditor().exists()).toBe(true);
+ });
+
+ it('editor contains the value provided', () => {
+ expect(findEditor().props('value')).toBe(mockCiYml);
+ });
+
+ it('editor is configured for the CI config path', () => {
+ expect(findEditor().props('fileName')).toBe(mockCiConfigPath);
+ });
+
+ it('bubbles up events', () => {
+ findEditor().vm.$emit(EDITOR_READY_EVENT);
+
+ expect(editorReadyListener).toHaveBeenCalled();
+ });
+ });
+
+ describe('register CI schema', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ // Since the editor will have already mounted, the event will have fired.
+ // To ensure we properly test this, we clear the mock and re-remit the event.
+ mockRegisterCiSchema.mockClear();
+ mockUse.mockClear();
+
+ findEditor().vm.$emit(EDITOR_READY_EVENT);
+ });
+
+ it('configures editor with syntax highlight', async () => {
+ expect(mockUse).toHaveBeenCalledTimes(1);
+ expect(mockRegisterCiSchema).toHaveBeenCalledTimes(1);
+ expect(mockRegisterCiSchema).toHaveBeenCalledWith({
+ projectNamespace: mockProjectNamespace,
+ projectPath: mockProjectPath,
+ ref: mockCommitSha,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pipeline_editor/components/header/pipeline_editor_header_spec.js b/spec/frontend/pipeline_editor/components/header/pipeline_editor_header_spec.js
new file mode 100644
index 00000000000..df15a6c8e7f
--- /dev/null
+++ b/spec/frontend/pipeline_editor/components/header/pipeline_editor_header_spec.js
@@ -0,0 +1,34 @@
+import { shallowMount } from '@vue/test-utils';
+import PipelineEditorHeader from '~/pipeline_editor/components/header/pipeline_editor_header.vue';
+import ValidationSegment from '~/pipeline_editor/components/header/validation_segment.vue';
+
+import { mockLintResponse } from '../../mock_data';
+
+describe('Pipeline editor header', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(PipelineEditorHeader, {
+ props: {
+ ciConfigData: mockLintResponse,
+ isCiConfigDataLoading: false,
+ },
+ });
+ };
+
+ const findValidationSegment = () => wrapper.findComponent(ValidationSegment);
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('template', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+ it('renders the validation segment', () => {
+ expect(findValidationSegment().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/pipeline_editor/components/info/validation_segment_spec.js b/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js
index 8a991d82018..cf1d89e1d7c 100644
--- a/spec/frontend/pipeline_editor/components/info/validation_segment_spec.js
+++ b/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js
@@ -1,9 +1,11 @@
-import { escape } from 'lodash';
-import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { escape } from 'lodash';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { sprintf } from '~/locale';
-import ValidationSegment, { i18n } from '~/pipeline_editor/components/info/validation_segment.vue';
+import ValidationSegment, {
+ i18n,
+} from '~/pipeline_editor/components/header/validation_segment.vue';
import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants';
import { mockYmlHelpPagePath, mergeUnwrappedCiConfig } from '../../mock_data';
@@ -29,6 +31,11 @@ describe('~/pipeline_editor/components/info/validation_segment.vue', () => {
const findLearnMoreLink = () => wrapper.findByTestId('learnMoreLink');
const findValidationMsg = () => wrapper.findByTestId('validationMsg');
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
it('shows the loading state', () => {
createComponent({ loading: true });
diff --git a/spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js b/spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js
index 5e9471376bd..6775433deb9 100644
--- a/spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js
+++ b/spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js
@@ -1,7 +1,7 @@
-import { shallowMount, mount } from '@vue/test-utils';
import { GlTable, GlLink } from '@gitlab/ui';
-import CiLintResults from '~/pipeline_editor/components/lint/ci_lint_results.vue';
+import { shallowMount, mount } from '@vue/test-utils';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import CiLintResults from '~/pipeline_editor/components/lint/ci_lint_results.vue';
import { mockJobs, mockErrors, mockWarnings } from '../../mock_data';
describe('CI Lint Results', () => {
diff --git a/spec/frontend/pipeline_editor/components/lint/ci_lint_spec.js b/spec/frontend/pipeline_editor/components/lint/ci_lint_spec.js
index 5ccf4bbdab4..fdddca3d62b 100644
--- a/spec/frontend/pipeline_editor/components/lint/ci_lint_spec.js
+++ b/spec/frontend/pipeline_editor/components/lint/ci_lint_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount, mount } from '@vue/test-utils';
import { GlAlert, GlLink } from '@gitlab/ui';
+import { shallowMount, mount } from '@vue/test-utils';
import CiLint from '~/pipeline_editor/components/lint/ci_lint.vue';
import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants';
import { mergeUnwrappedCiConfig, mockLintHelpPagePath } from '../../mock_data';
diff --git a/spec/frontend/pipeline_editor/components/lint/ci_lint_warnings_spec.js b/spec/frontend/pipeline_editor/components/lint/ci_lint_warnings_spec.js
index b441d26c146..4b576508ee9 100644
--- a/spec/frontend/pipeline_editor/components/lint/ci_lint_warnings_spec.js
+++ b/spec/frontend/pipeline_editor/components/lint/ci_lint_warnings_spec.js
@@ -1,5 +1,5 @@
-import { mount } from '@vue/test-utils';
import { GlAlert, GlSprintf } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import { trimText } from 'helpers/text_helper';
import CiLintWarnings from '~/pipeline_editor/components/lint/ci_lint_warnings.vue';
diff --git a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
new file mode 100644
index 00000000000..24af17e9ce6
--- /dev/null
+++ b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
@@ -0,0 +1,183 @@
+import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount, mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import CiConfigMergedPreview from '~/pipeline_editor/components/editor/ci_config_merged_preview.vue';
+import CiLint from '~/pipeline_editor/components/lint/ci_lint.vue';
+import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
+import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
+
+import { mockLintResponse, mockCiYml } from '../mock_data';
+
+describe('Pipeline editor tabs component', () => {
+ let wrapper;
+ const MockTextEditor = {
+ template: '<div />',
+ };
+ const mockProvide = {
+ glFeatures: {
+ ciConfigVisualizationTab: true,
+ ciConfigMergedTab: true,
+ },
+ };
+
+ const createComponent = ({ props = {}, provide = {}, mountFn = shallowMount } = {}) => {
+ wrapper = mountFn(PipelineEditorTabs, {
+ propsData: {
+ ciConfigData: mockLintResponse,
+ ciFileContent: mockCiYml,
+ isCiConfigDataLoading: false,
+ ...props,
+ },
+ provide: { ...mockProvide, ...provide },
+ stubs: {
+ TextEditor: MockTextEditor,
+ },
+ });
+ };
+
+ const findEditorTab = () => wrapper.find('[data-testid="editor-tab"]');
+ const findLintTab = () => wrapper.find('[data-testid="lint-tab"]');
+ const findMergedTab = () => wrapper.find('[data-testid="merged-tab"]');
+ const findVisualizationTab = () => wrapper.find('[data-testid="visualization-tab"]');
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findCiLint = () => wrapper.findComponent(CiLint);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findPipelineGraph = () => wrapper.findComponent(PipelineGraph);
+ const findTextEditor = () => wrapper.findComponent(MockTextEditor);
+ const findMergedPreview = () => wrapper.findComponent(CiConfigMergedPreview);
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('editor tab', () => {
+ it('displays editor only after the tab is mounted', async () => {
+ createComponent({ mountFn: mount });
+
+ expect(findTextEditor().exists()).toBe(false);
+
+ await nextTick();
+
+ expect(findTextEditor().exists()).toBe(true);
+ expect(findEditorTab().exists()).toBe(true);
+ });
+ });
+
+ describe('visualization tab', () => {
+ describe('with feature flag on', () => {
+ describe('while loading', () => {
+ beforeEach(() => {
+ createComponent({ props: { isCiConfigDataLoading: true } });
+ });
+
+ it('displays a loading icon if the lint query is loading', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(findPipelineGraph().exists()).toBe(false);
+ });
+ });
+ describe('after loading', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('display the tab and visualization', () => {
+ expect(findVisualizationTab().exists()).toBe(true);
+ expect(findPipelineGraph().exists()).toBe(true);
+ });
+ });
+ });
+
+ describe('with feature flag off', () => {
+ beforeEach(() => {
+ createComponent({
+ provide: {
+ glFeatures: { ciConfigVisualizationTab: false },
+ },
+ });
+ });
+
+ it('does not display the tab or component', () => {
+ expect(findVisualizationTab().exists()).toBe(false);
+ expect(findPipelineGraph().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('lint tab', () => {
+ describe('while loading', () => {
+ beforeEach(() => {
+ createComponent({ props: { isCiConfigDataLoading: true } });
+ });
+
+ it('displays a loading icon if the lint query is loading', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ it('does not display the lint component', () => {
+ expect(findCiLint().exists()).toBe(false);
+ });
+ });
+ describe('after loading', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('display the tab and the lint component', () => {
+ expect(findLintTab().exists()).toBe(true);
+ expect(findCiLint().exists()).toBe(true);
+ });
+ });
+ });
+
+ describe('merged tab', () => {
+ describe('with feature flag on', () => {
+ describe('while loading', () => {
+ beforeEach(() => {
+ createComponent({ props: { isCiConfigDataLoading: true } });
+ });
+
+ it('displays a loading icon if the lint query is loading', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+ });
+
+ describe('when `mergedYaml` is undefined', () => {
+ beforeEach(() => {
+ createComponent({ props: { ciConfigData: {} } });
+ });
+
+ it('show an error message', () => {
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(wrapper.vm.$options.errorTexts.loadMergedYaml);
+ });
+
+ it('does not render the `meged_preview` component', () => {
+ expect(findMergedPreview().exists()).toBe(false);
+ });
+ });
+
+ describe('after loading', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('display the tab and the merged preview component', () => {
+ expect(findMergedTab().exists()).toBe(true);
+ expect(findMergedPreview().exists()).toBe(true);
+ });
+ });
+ });
+ describe('with feature flag off', () => {
+ beforeEach(() => {
+ createComponent({ provide: { glFeatures: { ciConfigMergedTab: false } } });
+ });
+
+ it('does not display the merged tab', () => {
+ expect(findMergedTab().exists()).toBe(false);
+ expect(findMergedPreview().exists()).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pipeline_editor/components/text_editor_spec.js b/spec/frontend/pipeline_editor/components/text_editor_spec.js
deleted file mode 100644
index 9221d64c44b..00000000000
--- a/spec/frontend/pipeline_editor/components/text_editor_spec.js
+++ /dev/null
@@ -1,93 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import {
- mockCiConfigPath,
- mockCiYml,
- mockCommitSha,
- mockProjectPath,
- mockProjectNamespace,
-} from '../mock_data';
-
-import TextEditor from '~/pipeline_editor/components/text_editor.vue';
-
-describe('~/pipeline_editor/components/text_editor.vue', () => {
- let wrapper;
-
- let editorReadyListener;
- let mockUse;
- let mockRegisterCiSchema;
-
- const MockEditorLite = {
- template: '<div/>',
- props: ['value', 'fileName'],
- mounted() {
- this.$emit('editor-ready');
- },
- methods: {
- getEditor: () => ({
- use: mockUse,
- registerCiSchema: mockRegisterCiSchema,
- }),
- },
- };
-
- const createComponent = (opts = {}, mountFn = shallowMount) => {
- wrapper = mountFn(TextEditor, {
- provide: {
- projectPath: mockProjectPath,
- projectNamespace: mockProjectNamespace,
- },
- propsData: {
- ciConfigPath: mockCiConfigPath,
- commitSha: mockCommitSha,
- },
- attrs: {
- value: mockCiYml,
- },
- listeners: {
- 'editor-ready': editorReadyListener,
- },
- stubs: {
- EditorLite: MockEditorLite,
- },
- ...opts,
- });
- };
-
- const findEditor = () => wrapper.find(MockEditorLite);
-
- beforeEach(() => {
- editorReadyListener = jest.fn();
- mockUse = jest.fn();
- mockRegisterCiSchema = jest.fn();
-
- createComponent();
- });
-
- it('contains an editor', () => {
- expect(findEditor().exists()).toBe(true);
- });
-
- it('editor contains the value provided', () => {
- expect(findEditor().props('value')).toBe(mockCiYml);
- });
-
- it('editor is configured for the CI config path', () => {
- expect(findEditor().props('fileName')).toBe(mockCiConfigPath);
- });
-
- it('editor is configured with syntax highligting', async () => {
- expect(mockUse).toHaveBeenCalledTimes(1);
- expect(mockRegisterCiSchema).toHaveBeenCalledTimes(1);
- expect(mockRegisterCiSchema).toHaveBeenCalledWith({
- projectNamespace: mockProjectNamespace,
- projectPath: mockProjectPath,
- ref: mockCommitSha,
- });
- });
-
- it('bubbles up events', () => {
- findEditor().vm.$emit('editor-ready');
-
- expect(editorReadyListener).toHaveBeenCalled();
- });
-});
diff --git a/spec/frontend/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js b/spec/frontend/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js
new file mode 100644
index 00000000000..44fda2812d8
--- /dev/null
+++ b/spec/frontend/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js
@@ -0,0 +1,42 @@
+import { shallowMount } from '@vue/test-utils';
+import ConfirmDialog from '~/pipeline_editor/components/ui/confirm_unsaved_changes_dialog.vue';
+
+describe('pipeline_editor/components/ui/confirm_unsaved_changes_dialog', () => {
+ let beforeUnloadEvent;
+ let setDialogContent;
+ let wrapper;
+
+ const createComponent = (propsData = {}) => {
+ wrapper = shallowMount(ConfirmDialog, {
+ propsData,
+ });
+ };
+
+ beforeEach(() => {
+ beforeUnloadEvent = new Event('beforeunload');
+ jest.spyOn(beforeUnloadEvent, 'preventDefault');
+ setDialogContent = jest.spyOn(beforeUnloadEvent, 'returnValue', 'set');
+ });
+
+ afterEach(() => {
+ beforeUnloadEvent.preventDefault.mockRestore();
+ setDialogContent.mockRestore();
+ wrapper.destroy();
+ });
+
+ it('shows confirmation dialog when there are unsaved changes', () => {
+ createComponent({ hasUnsavedChanges: true });
+ window.dispatchEvent(beforeUnloadEvent);
+
+ expect(beforeUnloadEvent.preventDefault).toHaveBeenCalled();
+ expect(setDialogContent).toHaveBeenCalledWith('');
+ });
+
+ it('does not show confirmation dialog when there are no unsaved changes', () => {
+ createComponent({ hasUnsavedChanges: false });
+ window.dispatchEvent(beforeUnloadEvent);
+
+ expect(beforeUnloadEvent.preventDefault).not.toHaveBeenCalled();
+ expect(setDialogContent).not.toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js b/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js
index d3d9bf08209..291468c5229 100644
--- a/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js
+++ b/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js
@@ -1,6 +1,6 @@
-import { nextTick } from 'vue';
-import { mount } from '@vue/test-utils';
import { GlTabs } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import EditorTab from '~/pipeline_editor/components/ui/editor_tab.vue';
diff --git a/spec/frontend/pipeline_editor/graphql/resolvers_spec.js b/spec/frontend/pipeline_editor/graphql/resolvers_spec.js
index 3e008527415..d39c0d80296 100644
--- a/spec/frontend/pipeline_editor/graphql/resolvers_spec.js
+++ b/spec/frontend/pipeline_editor/graphql/resolvers_spec.js
@@ -1,5 +1,8 @@
import MockAdapter from 'axios-mock-adapter';
import Api from '~/api';
+import axios from '~/lib/utils/axios_utils';
+import httpStatus from '~/lib/utils/http_status';
+import { resolvers } from '~/pipeline_editor/graphql/resolvers';
import {
mockCiConfigPath,
mockCiYml,
@@ -7,9 +10,6 @@ import {
mockLintResponse,
mockProjectFullPath,
} from '../mock_data';
-import httpStatus from '~/lib/utils/http_status';
-import axios from '~/lib/utils/axios_utils';
-import { resolvers } from '~/pipeline_editor/graphql/resolvers';
jest.mock('~/api', () => {
return {
diff --git a/spec/frontend/pipeline_editor/mock_data.js b/spec/frontend/pipeline_editor/mock_data.js
index 3eacc467c51..8e248c11b87 100644
--- a/spec/frontend/pipeline_editor/mock_data.js
+++ b/spec/frontend/pipeline_editor/mock_data.js
@@ -54,6 +54,7 @@ export const mockCiConfigQueryResponse = {
data: {
ciConfig: {
errors: [],
+ mergedYaml: mockCiYml,
status: CI_CONFIG_STATUS_VALID,
stages: {
__typename: 'CiConfigStageConnection',
@@ -139,6 +140,8 @@ export const mergeUnwrappedCiConfig = (mergedConfig) => {
export const mockLintResponse = {
valid: true,
+ mergedYaml: mockCiYml,
+ status: CI_CONFIG_STATUS_VALID,
errors: [],
warnings: [],
jobs: [
diff --git a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
index d6b90900600..46d0452f437 100644
--- a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
+++ b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
@@ -1,119 +1,70 @@
-import { nextTick } from 'vue';
-import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
-import { GlAlert, GlButton, GlFormInput, GlFormTextarea, GlLoadingIcon, GlTabs } from '@gitlab/ui';
-import waitForPromises from 'helpers/wait_for_promises';
+import { GlAlert, GlButton, GlLoadingIcon, GlTabs } from '@gitlab/ui';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
-
+import waitForPromises from 'helpers/wait_for_promises';
import httpStatusCodes from '~/lib/utils/http_status';
-import { objectToQuery, redirectTo, refreshCurrentPage } from '~/lib/utils/url_utility';
+import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
+import TextEditor from '~/pipeline_editor/components/editor/text_editor.vue';
+
+import { COMMIT_SUCCESS, COMMIT_FAILURE, LOAD_FAILURE_UNKNOWN } from '~/pipeline_editor/constants';
+import getCiConfigData from '~/pipeline_editor/graphql/queries/ci_config.graphql';
+import PipelineEditorApp from '~/pipeline_editor/pipeline_editor_app.vue';
+import PipelineEditorHome from '~/pipeline_editor/pipeline_editor_home.vue';
import {
mockCiConfigPath,
mockCiConfigQueryResponse,
mockCiYml,
- mockCommitSha,
- mockCommitNextSha,
- mockCommitMessage,
mockDefaultBranch,
- mockProjectPath,
mockProjectFullPath,
- mockProjectNamespace,
- mockNewMergeRequestPath,
} from './mock_data';
-import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
-import getCiConfigData from '~/pipeline_editor/graphql/queries/ci_config.graphql';
-import EditorTab from '~/pipeline_editor/components/ui/editor_tab.vue';
-import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
-import PipelineEditorApp from '~/pipeline_editor/pipeline_editor_app.vue';
-import TextEditor from '~/pipeline_editor/components/text_editor.vue';
-
const localVue = createLocalVue();
localVue.use(VueApollo);
-jest.mock('~/lib/utils/url_utility', () => ({
- redirectTo: jest.fn(),
- refreshCurrentPage: jest.fn(),
- objectToQuery: jest.requireActual('~/lib/utils/url_utility').objectToQuery,
- mergeUrlParams: jest.requireActual('~/lib/utils/url_utility').mergeUrlParams,
-}));
-
const MockEditorLite = {
template: '<div/>',
};
const mockProvide = {
+ ciConfigPath: mockCiConfigPath,
+ defaultBranch: mockDefaultBranch,
projectFullPath: mockProjectFullPath,
- projectPath: mockProjectPath,
- projectNamespace: mockProjectNamespace,
- glFeatures: {
- ciConfigVisualizationTab: true,
- },
};
-describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
+describe('Pipeline editor app component', () => {
let wrapper;
let mockApollo;
let mockBlobContentData;
let mockCiConfigData;
- let mockMutate;
-
- const createComponent = ({
- props = {},
- blobLoading = false,
- lintLoading = false,
- options = {},
- mountFn = shallowMount,
- provide = mockProvide,
- } = {}) => {
- mockMutate = jest.fn().mockResolvedValue({
- data: {
- commitCreate: {
- errors: [],
- commit: {
- sha: mockCommitNextSha,
- },
- },
- },
- });
- wrapper = mountFn(PipelineEditorApp, {
- propsData: {
- ciConfigPath: mockCiConfigPath,
- commitSha: mockCommitSha,
- defaultBranch: mockDefaultBranch,
- newMergeRequestPath: mockNewMergeRequestPath,
- ...props,
- },
- provide,
+ const createComponent = ({ blobLoading = false, options = {} } = {}) => {
+ wrapper = shallowMount(PipelineEditorApp, {
+ provide: mockProvide,
stubs: {
GlTabs,
GlButton,
CommitForm,
EditorLite: MockEditorLite,
- TextEditor,
},
mocks: {
$apollo: {
queries: {
- content: {
+ initialCiFileContent: {
loading: blobLoading,
},
ciConfigData: {
- loading: lintLoading,
+ loading: false,
},
},
- mutate: mockMutate,
},
},
- // attachTo is required for input/submit events
- attachTo: mountFn === mount ? document.body : null,
...options,
});
};
- const createComponentWithApollo = ({ props = {}, mountFn = shallowMount } = {}) => {
+ const createComponentWithApollo = ({ props = {} } = {}) => {
const handlers = [[getCiConfigData, mockCiConfigData]];
const resolvers = {
Query: {
@@ -134,18 +85,13 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
apolloProvider: mockApollo,
};
- createComponent({ props, options }, mountFn);
+ createComponent({ props, options });
};
- const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
- const findAlert = () => wrapper.find(GlAlert);
- const findTabAt = (i) => wrapper.findAll(EditorTab).at(i);
- const findVisualizationTab = () => wrapper.find('[data-testid="visualization-tab"]');
- const findTextEditor = () => wrapper.find(TextEditor);
- const findEditorLite = () => wrapper.find(MockEditorLite);
- const findCommitForm = () => wrapper.find(CommitForm);
- const findPipelineGraph = () => wrapper.find(PipelineGraph);
- const findCommitBtnLoadingIcon = () => wrapper.find('[type="submit"]').find(GlLoadingIcon);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findEditorHome = () => wrapper.findComponent(PipelineEditorHome);
+ const findTextEditor = () => wrapper.findComponent(TextEditor);
beforeEach(() => {
mockBlobContentData = jest.fn();
@@ -155,9 +101,6 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
afterEach(() => {
mockBlobContentData.mockReset();
mockCiConfigData.mockReset();
- refreshCurrentPage.mockReset();
- redirectTo.mockReset();
- mockMutate.mockReset();
wrapper.destroy();
wrapper = null;
@@ -170,245 +113,6 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
expect(findTextEditor().exists()).toBe(false);
});
- describe('tabs', () => {
- describe('editor tab', () => {
- it('displays editor only after the tab is mounted', async () => {
- createComponent({ mountFn: mount });
-
- expect(findTabAt(0).find(TextEditor).exists()).toBe(false);
-
- await nextTick();
-
- expect(findTabAt(0).find(TextEditor).exists()).toBe(true);
- });
- });
-
- describe('visualization tab', () => {
- describe('with feature flag on', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('display the tab', () => {
- expect(findVisualizationTab().exists()).toBe(true);
- });
-
- it('displays a loading icon if the lint query is loading', () => {
- createComponent({ lintLoading: true });
-
- expect(findLoadingIcon().exists()).toBe(true);
- expect(findPipelineGraph().exists()).toBe(false);
- });
- });
-
- describe('with feature flag off', () => {
- beforeEach(() => {
- createComponent({
- provide: {
- ...mockProvide,
- glFeatures: { ciConfigVisualizationTab: false },
- },
- });
- });
-
- it('does not display the tab', () => {
- expect(findVisualizationTab().exists()).toBe(false);
- });
- });
- });
- });
-
- describe('when data is set', () => {
- beforeEach(async () => {
- createComponent({ mountFn: mount });
-
- wrapper.setData({
- content: mockCiYml,
- contentModel: mockCiYml,
- });
-
- await waitForPromises();
- });
-
- it('displays content after the query loads', () => {
- expect(findLoadingIcon().exists()).toBe(false);
-
- expect(findEditorLite().attributes('value')).toBe(mockCiYml);
- expect(findEditorLite().attributes('file-name')).toBe(mockCiConfigPath);
- });
-
- it('configures text editor', () => {
- expect(findTextEditor().props('commitSha')).toBe(mockCommitSha);
- });
-
- describe('commit form', () => {
- const mockVariables = {
- content: mockCiYml,
- filePath: mockCiConfigPath,
- lastCommitId: mockCommitSha,
- message: mockCommitMessage,
- projectPath: mockProjectFullPath,
- startBranch: mockDefaultBranch,
- };
-
- const findInForm = (selector) => findCommitForm().find(selector);
-
- const submitCommit = async ({
- message = mockCommitMessage,
- branch = mockDefaultBranch,
- openMergeRequest = false,
- } = {}) => {
- await findInForm(GlFormTextarea).setValue(message);
- await findInForm(GlFormInput).setValue(branch);
- if (openMergeRequest) {
- await findInForm('[data-testid="new-mr-checkbox"]').setChecked(openMergeRequest);
- }
- await findInForm('[type="submit"]').trigger('click');
- };
-
- const cancelCommitForm = async () => {
- const findCancelBtn = () => wrapper.find('[type="reset"]');
- await findCancelBtn().trigger('click');
- };
-
- describe('when the user commits changes to the current branch', () => {
- beforeEach(async () => {
- await submitCommit();
- });
-
- it('calls the mutation with the default branch', () => {
- expect(mockMutate).toHaveBeenCalledWith({
- mutation: expect.any(Object),
- variables: {
- ...mockVariables,
- branch: mockDefaultBranch,
- },
- });
- });
-
- it('displays an alert to indicate success', () => {
- expect(findAlert().text()).toMatchInterpolatedText(
- 'Your changes have been successfully committed.',
- );
- });
-
- it('shows no saving state', () => {
- expect(findCommitBtnLoadingIcon().exists()).toBe(false);
- });
-
- it('a second commit submits the latest sha, keeping the form updated', async () => {
- await submitCommit();
-
- expect(mockMutate).toHaveBeenCalledTimes(2);
- expect(mockMutate).toHaveBeenLastCalledWith({
- mutation: expect.any(Object),
- variables: {
- ...mockVariables,
- lastCommitId: mockCommitNextSha,
- branch: mockDefaultBranch,
- },
- });
- });
- });
-
- describe('when the user commits changes to a new branch', () => {
- const newBranch = 'new-branch';
-
- beforeEach(async () => {
- await submitCommit({
- branch: newBranch,
- });
- });
-
- it('calls the mutation with the new branch', () => {
- expect(mockMutate).toHaveBeenCalledWith({
- mutation: expect.any(Object),
- variables: {
- ...mockVariables,
- branch: newBranch,
- },
- });
- });
- });
-
- describe('when the user commits changes to open a new merge request', () => {
- const newBranch = 'new-branch';
-
- beforeEach(async () => {
- await submitCommit({
- branch: newBranch,
- openMergeRequest: true,
- });
- });
-
- it('redirects to the merge request page with source and target branches', () => {
- const branchesQuery = objectToQuery({
- 'merge_request[source_branch]': newBranch,
- 'merge_request[target_branch]': mockDefaultBranch,
- });
-
- expect(redirectTo).toHaveBeenCalledWith(`${mockNewMergeRequestPath}?${branchesQuery}`);
- });
- });
-
- describe('when the commit is ocurring', () => {
- it('shows a saving state', async () => {
- await mockMutate.mockImplementationOnce(() => {
- expect(findCommitBtnLoadingIcon().exists()).toBe(true);
- return Promise.resolve();
- });
-
- await submitCommit({
- message: mockCommitMessage,
- branch: mockDefaultBranch,
- openMergeRequest: false,
- });
- });
- });
-
- describe('when the commit fails', () => {
- it('shows an error message', async () => {
- mockMutate.mockRejectedValueOnce(new Error('commit failed'));
-
- await submitCommit();
-
- await waitForPromises();
-
- expect(findAlert().text()).toMatchInterpolatedText(
- 'The GitLab CI configuration could not be updated. commit failed',
- );
- });
-
- it('shows an unkown error', async () => {
- mockMutate.mockRejectedValueOnce();
-
- await submitCommit();
-
- await waitForPromises();
-
- expect(findAlert().text()).toMatchInterpolatedText(
- 'The GitLab CI configuration could not be updated.',
- );
- });
- });
-
- describe('when the commit form is cancelled', () => {
- const otherContent = 'other content';
-
- beforeEach(async () => {
- findTextEditor().vm.$emit('input', otherContent);
- await nextTick();
- });
-
- it('content is restored after cancel is called', async () => {
- await cancelCommitForm();
-
- expect(findEditorLite().attributes('value')).toBe(mockCiYml);
- });
- });
- });
- });
-
describe('when queries are called', () => {
beforeEach(() => {
mockBlobContentData.mockResolvedValue(mockCiYml);
@@ -422,14 +126,12 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
await waitForPromises();
});
- it('shows editor and commit form', () => {
- expect(findEditorLite().exists()).toBe(true);
- expect(findTextEditor().exists()).toBe(true);
+ it('shows pipeline editor home component', () => {
+ expect(findEditorHome().exists()).toBe(true);
});
- it('no error is shown when data is set', async () => {
+ it('no error is shown when data is set', () => {
expect(findAlert().exists()).toBe(false);
- expect(findEditorLite().attributes('value')).toBe(mockCiYml);
});
it('ci config query is called with correct variables', async () => {
@@ -445,10 +147,10 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
});
describe('when no file exists', () => {
- const expectedAlertMsg =
+ const noFileAlertMsg =
'There is no .gitlab-ci.yml file in this repository, please add one and visit the Pipeline Editor again.';
- it('shows a 404 error message and does not show editor or commit form', async () => {
+ it('shows a 404 error message and does not show editor home component', async () => {
mockBlobContentData.mockRejectedValueOnce({
response: {
status: httpStatusCodes.NOT_FOUND,
@@ -458,12 +160,11 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
await waitForPromises();
- expect(findAlert().text()).toBe(expectedAlertMsg);
- expect(findEditorLite().exists()).toBe(false);
- expect(findTextEditor().exists()).toBe(false);
+ expect(findAlert().text()).toBe(noFileAlertMsg);
+ expect(findEditorHome().exists()).toBe(false);
});
- it('shows a 400 error message and does not show editor or commit form', async () => {
+ it('shows a 400 error message and does not show editor home component', async () => {
mockBlobContentData.mockRejectedValueOnce({
response: {
status: httpStatusCodes.BAD_REQUEST,
@@ -473,9 +174,8 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
await waitForPromises();
- expect(findAlert().text()).toBe(expectedAlertMsg);
- expect(findEditorLite().exists()).toBe(false);
- expect(findTextEditor().exists()).toBe(false);
+ expect(findAlert().text()).toBe(noFileAlertMsg);
+ expect(findEditorHome().exists()).toBe(false);
});
it('shows a unkown error message', async () => {
@@ -483,9 +183,60 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
createComponentWithApollo();
await waitForPromises();
- expect(findAlert().text()).toBe('The CI configuration was not loaded, please try again.');
- expect(findEditorLite().exists()).toBe(true);
- expect(findTextEditor().exists()).toBe(true);
+ expect(findAlert().text()).toBe(wrapper.vm.$options.errorTexts[LOAD_FAILURE_UNKNOWN]);
+ expect(findEditorHome().exists()).toBe(true);
+ });
+ });
+
+ describe('when the user commits', () => {
+ const updateFailureMessage = 'The GitLab CI configuration could not be updated.';
+
+ describe('and the commit mutation succeeds', () => {
+ beforeEach(() => {
+ createComponent();
+
+ findEditorHome().vm.$emit('commit', { type: COMMIT_SUCCESS });
+ });
+
+ it('shows a confirmation message', () => {
+ expect(findAlert().text()).toBe(wrapper.vm.$options.successTexts[COMMIT_SUCCESS]);
+ });
+ });
+ describe('and the commit mutation fails', () => {
+ const commitFailedReasons = ['Commit failed'];
+
+ beforeEach(() => {
+ createComponent();
+
+ findEditorHome().vm.$emit('showError', {
+ type: COMMIT_FAILURE,
+ reasons: commitFailedReasons,
+ });
+ });
+
+ it('shows an error message', () => {
+ expect(findAlert().text()).toMatchInterpolatedText(
+ `${updateFailureMessage} ${commitFailedReasons[0]}`,
+ );
+ });
+ });
+ describe('when an unknown error occurs', () => {
+ const unknownReasons = ['Commit failed'];
+
+ beforeEach(() => {
+ createComponent();
+
+ findEditorHome().vm.$emit('showError', {
+ type: COMMIT_FAILURE,
+ reasons: unknownReasons,
+ });
+ });
+
+ it('shows an error message', () => {
+ expect(findAlert().text()).toMatchInterpolatedText(
+ `${updateFailureMessage} ${unknownReasons[0]}`,
+ );
+ });
});
});
});
diff --git a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
new file mode 100644
index 00000000000..9864f3c13f9
--- /dev/null
+++ b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
@@ -0,0 +1,78 @@
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+
+import CommitSection from '~/pipeline_editor/components/commit/commit_section.vue';
+import PipelineEditorHeader from '~/pipeline_editor/components/header/pipeline_editor_header.vue';
+import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
+import { MERGED_TAB, VISUALIZE_TAB } from '~/pipeline_editor/constants';
+import PipelineEditorHome from '~/pipeline_editor/pipeline_editor_home.vue';
+
+import { mockLintResponse, mockCiYml } from './mock_data';
+
+describe('Pipeline editor home wrapper', () => {
+ let wrapper;
+
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = shallowMount(PipelineEditorHome, {
+ propsData: {
+ ciConfigData: mockLintResponse,
+ ciFileContent: mockCiYml,
+ isCiConfigDataLoading: false,
+ ...props,
+ },
+ });
+ };
+
+ const findPipelineEditorHeader = () => wrapper.findComponent(PipelineEditorHeader);
+ const findPipelineEditorTabs = () => wrapper.findComponent(PipelineEditorTabs);
+ const findCommitSection = () => wrapper.findComponent(CommitSection);
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('renders', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('shows the pipeline editor header', () => {
+ expect(findPipelineEditorHeader().exists()).toBe(true);
+ });
+
+ it('shows the pipeline editor tabs', () => {
+ expect(findPipelineEditorTabs().exists()).toBe(true);
+ });
+
+ it('shows the commit section by default', () => {
+ expect(findCommitSection().exists()).toBe(true);
+ });
+ });
+
+ describe('commit form toggle', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('hides the commit form when in the merged tab', async () => {
+ expect(findCommitSection().exists()).toBe(true);
+
+ findPipelineEditorTabs().vm.$emit('set-current-tab', MERGED_TAB);
+ await nextTick();
+ expect(findCommitSection().exists()).toBe(false);
+ });
+
+ it('shows the form again when leaving the merged tab', async () => {
+ expect(findCommitSection().exists()).toBe(true);
+
+ findPipelineEditorTabs().vm.$emit('set-current-tab', MERGED_TAB);
+ await nextTick();
+ expect(findCommitSection().exists()).toBe(false);
+
+ findPipelineEditorTabs().vm.$emit('set-current-tab', VISUALIZE_TAB);
+ await nextTick();
+ expect(findCommitSection().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
index 421ad9f4939..51bb0ecee9c 100644
--- a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
+++ b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
@@ -1,9 +1,10 @@
-import { mount, shallowMount } from '@vue/test-utils';
import { GlDropdown, GlDropdownItem, GlForm, GlSprintf, GlLoadingIcon } from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
-import httpStatusCodes from '~/lib/utils/http_status';
import axios from '~/lib/utils/axios_utils';
+import httpStatusCodes from '~/lib/utils/http_status';
+import { redirectTo } from '~/lib/utils/url_utility';
import PipelineNewForm from '~/pipeline_new/components/pipeline_new_form.vue';
import {
mockBranches,
@@ -13,7 +14,6 @@ import {
mockProjectId,
mockError,
} from '../mock_data';
-import { redirectTo } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility', () => ({
redirectTo: jest.fn(),
@@ -34,6 +34,7 @@ describe('Pipeline New Form', () => {
const findForm = () => wrapper.find(GlForm);
const findDropdown = () => wrapper.find(GlDropdown);
const findDropdownItems = () => wrapper.findAll(GlDropdownItem);
+ const findSubmitButton = () => wrapper.find('[data-testid="run_pipeline_button"]');
const findVariableRows = () => wrapper.findAll('[data-testid="ci-variable-row"]');
const findRemoveIcons = () => wrapper.findAll('[data-testid="remove-ci-variable-row"]');
const findKeyInputs = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-key"]');
@@ -155,6 +156,18 @@ describe('Pipeline New Form', () => {
await waitForPromises();
});
+
+ it('disables the submit button immediately after submitting', async () => {
+ createComponent();
+
+ expect(findSubmitButton().props('disabled')).toBe(false);
+
+ findForm().vm.$emit('submit', dummySubmitEvent);
+ await waitForPromises();
+
+ expect(findSubmitButton().props('disabled')).toBe(true);
+ });
+
it('creates pipeline with full ref and variables', async () => {
createComponent();
@@ -167,6 +180,7 @@ describe('Pipeline New Form', () => {
expect(getExpectedPostParams().ref).toEqual(wrapper.vm.$data.refValue.fullName);
expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${postResponse.id}`);
});
+
it('creates a pipeline with short ref and variables', async () => {
// query params are used
createComponent('', mockParams);
@@ -225,42 +239,47 @@ describe('Pipeline New Form', () => {
});
});
- describe('when feature flag new_pipeline_form_prefilled_vars is enabled', () => {
- let origGon;
-
+ describe('when yml defines a variable', () => {
const mockYmlKey = 'yml_var';
const mockYmlValue = 'yml_var_val';
+ const mockYmlMultiLineValue = `A value
+ with multiple
+ lines`;
const mockYmlDesc = 'A var from yml.';
- beforeAll(() => {
- origGon = window.gon;
- window.gon = { features: { newPipelineFormPrefilledVars: true } };
- });
+ it('loading icon is shown when content is requested and hidden when received', async () => {
+ createComponent('', mockParams, mount);
- afterAll(() => {
- window.gon = origGon;
- });
+ mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
+ [mockYmlKey]: {
+ value: mockYmlValue,
+ description: mockYmlDesc,
+ },
+ });
- describe('loading state', () => {
- it('loading icon is shown when content is requested and hidden when received', async () => {
- createComponent('', mockParams, mount);
+ expect(findLoadingIcon().exists()).toBe(true);
- mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
- [mockYmlKey]: {
- value: mockYmlValue,
- description: mockYmlDesc,
- },
- });
+ await waitForPromises();
- expect(findLoadingIcon().exists()).toBe(true);
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
- await waitForPromises();
+ it('multi-line strings are added to the value field without removing line breaks', async () => {
+ createComponent('', mockParams, mount);
- expect(findLoadingIcon().exists()).toBe(false);
+ mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
+ [mockYmlKey]: {
+ value: mockYmlMultiLineValue,
+ description: mockYmlDesc,
+ },
});
+
+ await waitForPromises();
+
+ expect(findValueInputs().at(0).element.value).toBe(mockYmlMultiLineValue);
});
- describe('when yml defines a variable with description', () => {
+ describe('with description', () => {
beforeEach(async () => {
createComponent('', mockParams, mount);
@@ -302,7 +321,7 @@ describe('Pipeline New Form', () => {
});
});
- describe('when yml defines a variable without description', () => {
+ describe('without description', () => {
beforeEach(async () => {
createComponent('', mockParams, mount);
@@ -325,31 +344,55 @@ describe('Pipeline New Form', () => {
describe('Form errors and warnings', () => {
beforeEach(() => {
createComponent();
+ });
- mock.onPost(pipelinesPath).reply(httpStatusCodes.BAD_REQUEST, mockError);
+ describe('when the error response can be handled', () => {
+ beforeEach(async () => {
+ mock.onPost(pipelinesPath).reply(httpStatusCodes.BAD_REQUEST, mockError);
- findForm().vm.$emit('submit', dummySubmitEvent);
+ findForm().vm.$emit('submit', dummySubmitEvent);
- return waitForPromises();
- });
+ await waitForPromises();
+ });
- it('shows both error and warning', () => {
- expect(findErrorAlert().exists()).toBe(true);
- expect(findWarningAlert().exists()).toBe(true);
- });
+ it('shows both error and warning', () => {
+ expect(findErrorAlert().exists()).toBe(true);
+ expect(findWarningAlert().exists()).toBe(true);
+ });
- it('shows the correct error', () => {
- expect(findErrorAlert().text()).toBe(mockError.errors[0]);
- });
+ it('shows the correct error', () => {
+ expect(findErrorAlert().text()).toBe(mockError.errors[0]);
+ });
- it('shows the correct warning title', () => {
- const { length } = mockError.warnings;
+ it('shows the correct warning title', () => {
+ const { length } = mockError.warnings;
+
+ expect(findWarningAlertSummary().attributes('message')).toBe(`${length} warnings found:`);
+ });
- expect(findWarningAlertSummary().attributes('message')).toBe(`${length} warnings found:`);
+ it('shows the correct amount of warnings', () => {
+ expect(findWarnings()).toHaveLength(mockError.warnings.length);
+ });
+
+ it('re-enables the submit button', () => {
+ expect(findSubmitButton().props('disabled')).toBe(false);
+ });
});
- it('shows the correct amount of warnings', () => {
- expect(findWarnings()).toHaveLength(mockError.warnings.length);
+ describe('when the error response cannot be handled', () => {
+ beforeEach(async () => {
+ mock
+ .onPost(pipelinesPath)
+ .reply(httpStatusCodes.INTERNAL_SERVER_ERROR, 'something went wrong');
+
+ findForm().vm.$emit('submit', dummySubmitEvent);
+
+ await waitForPromises();
+ });
+
+ it('re-enables the submit button', () => {
+ expect(findSubmitButton().props('disabled')).toBe(false);
+ });
});
});
});
diff --git a/spec/frontend/pipeline_new/utils/format_refs_spec.js b/spec/frontend/pipeline_new/utils/format_refs_spec.js
index 1fda6a8af83..405a747c3ba 100644
--- a/spec/frontend/pipeline_new/utils/format_refs_spec.js
+++ b/spec/frontend/pipeline_new/utils/format_refs_spec.js
@@ -1,5 +1,5 @@
-import formatRefs from '~/pipeline_new/utils/format_refs';
import { BRANCH_REF_TYPE, TAG_REF_TYPE } from '~/pipeline_new/constants';
+import formatRefs from '~/pipeline_new/utils/format_refs';
import { mockBranchRefs, mockTagRefs } from '../mock_data';
describe('Format refs util', () => {
diff --git a/spec/frontend/pipelines/blank_state_spec.js b/spec/frontend/pipelines/blank_state_spec.js
index c09d9232569..5dcf3d267ed 100644
--- a/spec/frontend/pipelines/blank_state_spec.js
+++ b/spec/frontend/pipelines/blank_state_spec.js
@@ -1,25 +1,20 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import component from '~/pipelines/components/pipelines_list/blank_state.vue';
+import { getByText } from '@testing-library/dom';
+import { mount } from '@vue/test-utils';
+import BlankState from '~/pipelines/components/pipelines_list/blank_state.vue';
describe('Pipelines Blank State', () => {
- let vm;
- let Component;
-
- beforeEach(() => {
- Component = Vue.extend(component);
-
- vm = mountComponent(Component, {
+ const wrapper = mount(BlankState, {
+ propsData: {
svgPath: 'foo',
message: 'Blank State',
- });
+ },
});
it('should render svg', () => {
- expect(vm.$el.querySelector('.svg-content img').getAttribute('src')).toEqual('foo');
+ expect(wrapper.find('.svg-content img').attributes('src')).toEqual('foo');
});
it('should render message', () => {
- expect(vm.$el.querySelector('h4').textContent.trim()).toEqual('Blank State');
+ expect(getByText(wrapper.element, /Blank State/i)).toBeTruthy();
});
});
diff --git a/spec/frontend/pipelines/components/dag/dag_annotations_spec.js b/spec/frontend/pipelines/components/dag/dag_annotations_spec.js
index 80807c0b330..1941a7f2777 100644
--- a/spec/frontend/pipelines/components/dag/dag_annotations_spec.js
+++ b/spec/frontend/pipelines/components/dag/dag_annotations_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount, mount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
+import { shallowMount, mount } from '@vue/test-utils';
import DagAnnotations from '~/pipelines/components/dag/dag_annotations.vue';
import { singleNote, multiNote } from './mock_data';
diff --git a/spec/frontend/pipelines/components/dag/dag_graph_spec.js b/spec/frontend/pipelines/components/dag/dag_graph_spec.js
index ccfb2ae7cee..4619548d1bb 100644
--- a/spec/frontend/pipelines/components/dag/dag_graph_spec.js
+++ b/spec/frontend/pipelines/components/dag/dag_graph_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
-import DagGraph from '~/pipelines/components/dag/dag_graph.vue';
import { IS_HIGHLIGHTED, LINK_SELECTOR, NODE_SELECTOR } from '~/pipelines/components/dag/constants';
-import { highlightIn, highlightOut } from '~/pipelines/components/dag/interactions';
+import DagGraph from '~/pipelines/components/dag/dag_graph.vue';
import { createSankey } from '~/pipelines/components/dag/drawing_utils';
+import { highlightIn, highlightOut } from '~/pipelines/components/dag/interactions';
import { removeOrphanNodes } from '~/pipelines/components/parsing_utils';
import { parsedData } from './mock_data';
diff --git a/spec/frontend/pipelines/components/dag/dag_spec.js b/spec/frontend/pipelines/components/dag/dag_spec.js
index f6195e30e44..14030930657 100644
--- a/spec/frontend/pipelines/components/dag/dag_spec.js
+++ b/spec/frontend/pipelines/components/dag/dag_spec.js
@@ -1,10 +1,10 @@
-import { mount, shallowMount } from '@vue/test-utils';
import { GlAlert, GlEmptyState } from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
+import { ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from '~/pipelines/components/dag/constants';
import Dag from '~/pipelines/components/dag/dag.vue';
-import DagGraph from '~/pipelines/components/dag/dag_graph.vue';
import DagAnnotations from '~/pipelines/components/dag/dag_annotations.vue';
+import DagGraph from '~/pipelines/components/dag/dag_graph.vue';
-import { ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from '~/pipelines/components/dag/constants';
import { PARSE_FAILURE, UNSUPPORTED_DATA } from '~/pipelines/constants';
import {
mockParsedGraphQLNodes,
diff --git a/spec/frontend/pipelines/components/dag/parsing_utils_spec.js b/spec/frontend/pipelines/components/dag/parsing_utils_spec.js
index 5d3f680a57c..84ff83883b7 100644
--- a/spec/frontend/pipelines/components/dag/parsing_utils_spec.js
+++ b/spec/frontend/pipelines/components/dag/parsing_utils_spec.js
@@ -1,3 +1,4 @@
+import { createSankey } from '~/pipelines/components/dag/drawing_utils';
import {
createNodeDict,
makeLinksFromNodes,
@@ -7,7 +8,6 @@ import {
getMaxNodes,
} from '~/pipelines/components/parsing_utils';
-import { createSankey } from '~/pipelines/components/dag/drawing_utils';
import { mockParsedGraphQLNodes } from './mock_data';
describe('DAG visualization parsing utilities', () => {
diff --git a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
index 00fe9e784b3..e43aa2a02f5 100644
--- a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
+++ b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
@@ -1,6 +1,6 @@
+import { GlFilteredSearch } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import { GlFilteredSearch } from '@gitlab/ui';
import Api from '~/api';
import axios from '~/lib/utils/axios_utils';
import PipelinesFilteredSearch from '~/pipelines/components/pipelines_list/pipelines_filtered_search.vue';
diff --git a/spec/frontend/pipelines/empty_state_spec.js b/spec/frontend/pipelines/empty_state_spec.js
index 7e42a3b5ae9..3ebedc9ac87 100644
--- a/spec/frontend/pipelines/empty_state_spec.js
+++ b/spec/frontend/pipelines/empty_state_spec.js
@@ -1,7 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { withGonExperiment } from 'helpers/experimentation_helper';
import EmptyState from '~/pipelines/components/pipelines_list/empty_state.vue';
-import Tracking from '~/tracking';
describe('Pipelines Empty State', () => {
let wrapper;
@@ -40,104 +38,15 @@ describe('Pipelines Empty State', () => {
expect(findGetStartedButton().attributes('href')).toBe('foo');
});
- describe('when in control group', () => {
- it('should render empty state information', () => {
- expect(findInfoText()).toContain(
- 'Continuous Integration can help catch bugs by running your tests automatically',
- 'while Continuous Deployment can help you deliver code to your product environment',
- );
- });
-
- it('should render a button', () => {
- expect(findGetStartedButton().text()).toBe('Get started with Pipelines');
- });
- });
-
- describe('when in experiment group', () => {
- withGonExperiment('pipelinesEmptyState');
-
- beforeEach(() => {
- createWrapper();
- });
-
- it('should render empty state information', () => {
- expect(findInfoText()).toContain(
- 'GitLab CI/CD can automatically build, test, and deploy your code. Let GitLab take care of time',
- 'consuming tasks, so you can spend more time creating',
- );
- });
-
- it('should render button text', () => {
- expect(findGetStartedButton().text()).toBe('Get started with CI/CD');
- });
+ it('should render empty state information', () => {
+ expect(findInfoText()).toContain(
+ 'GitLab CI/CD can automatically build, test, and deploy your code. Let GitLab take care of time',
+ 'consuming tasks, so you can spend more time creating',
+ );
});
- describe('tracking', () => {
- let origGon;
-
- describe('when data is set', () => {
- beforeEach(() => {
- jest.spyOn(Tracking, 'event').mockImplementation(() => {});
- origGon = window.gon;
-
- window.gon = {
- tracking_data: {
- category: 'Growth::Activation::Experiment::PipelinesEmptyState',
- value: 1,
- property: 'experimental_group',
- label: 'label',
- },
- };
- createWrapper();
- });
-
- afterEach(() => {
- window.gon = origGon;
- });
-
- it('tracks when mounted', () => {
- expect(Tracking.event).toHaveBeenCalledWith(
- 'Growth::Activation::Experiment::PipelinesEmptyState',
- 'viewed',
- {
- value: 1,
- label: 'label',
- property: 'experimental_group',
- },
- );
- });
-
- it('tracks when button is clicked', () => {
- findGetStartedButton().vm.$emit('click');
-
- expect(Tracking.event).toHaveBeenCalledWith(
- 'Growth::Activation::Experiment::PipelinesEmptyState',
- 'documentation_clicked',
- {
- value: 1,
- label: 'label',
- property: 'experimental_group',
- },
- );
- });
- });
-
- describe('when no data is defined', () => {
- beforeEach(() => {
- jest.spyOn(Tracking, 'event').mockImplementation(() => {});
-
- createWrapper();
- });
-
- it('does not track on view', () => {
- expect(Tracking.event).not.toHaveBeenCalled();
- });
-
- it('does not track when button is clicked', () => {
- findGetStartedButton().vm.$emit('click');
- expect(Tracking.event).not.toHaveBeenCalled();
- });
- });
+ it('should render button text', () => {
+ expect(findGetStartedButton().text()).toBe('Get started with CI/CD');
});
});
});
diff --git a/spec/frontend/pipelines/graph/action_component_spec.js b/spec/frontend/pipelines/graph/action_component_spec.js
index 95d96e127c6..6a7018fa1e5 100644
--- a/spec/frontend/pipelines/graph/action_component_spec.js
+++ b/spec/frontend/pipelines/graph/action_component_spec.js
@@ -1,5 +1,5 @@
-import { mount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
diff --git a/spec/frontend/pipelines/graph/graph_component_legacy_spec.js b/spec/frontend/pipelines/graph/graph_component_legacy_spec.js
index 840b1f8baf5..a955572a481 100644
--- a/spec/frontend/pipelines/graph/graph_component_legacy_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_legacy_spec.js
@@ -1,14 +1,14 @@
-import { nextTick } from 'vue';
-import { mount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { setHTMLFixture } from 'helpers/fixtures';
-import PipelineStore from '~/pipelines/stores/pipeline_store';
import GraphComponentLegacy from '~/pipelines/components/graph/graph_component_legacy.vue';
-import StageColumnComponentLegacy from '~/pipelines/components/graph/stage_column_component_legacy.vue';
import LinkedPipelinesColumnLegacy from '~/pipelines/components/graph/linked_pipelines_column_legacy.vue';
-import graphJSON from './mock_data_legacy';
-import linkedPipelineJSON from './linked_pipelines_mock_data';
+import StageColumnComponentLegacy from '~/pipelines/components/graph/stage_column_component_legacy.vue';
import PipelinesMediator from '~/pipelines/pipeline_details_mediator';
+import PipelineStore from '~/pipelines/stores/pipeline_store';
+import linkedPipelineJSON from './linked_pipelines_mock_data';
+import graphJSON from './mock_data_legacy';
describe('graph component', () => {
let store;
diff --git a/spec/frontend/pipelines/graph/graph_component_spec.js b/spec/frontend/pipelines/graph/graph_component_spec.js
index cfc3b7af282..3e8d4ba314c 100644
--- a/spec/frontend/pipelines/graph/graph_component_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_spec.js
@@ -1,10 +1,10 @@
import { mount, shallowMount } from '@vue/test-utils';
+import { GRAPHQL } from '~/pipelines/components/graph/constants';
import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
-import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue';
import JobItem from '~/pipelines/components/graph/job_item.vue';
import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue';
+import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue';
import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
-import { GRAPHQL } from '~/pipelines/components/graph/constants';
import {
generateResponse,
mockPipelineResponse,
@@ -22,6 +22,13 @@ describe('graph component', () => {
pipeline: generateResponse(mockPipelineResponse, 'root/fungi-xoxo'),
};
+ const defaultData = {
+ measurements: {
+ width: 800,
+ height: 800,
+ },
+ };
+
const createComponent = ({
data = {},
mountFn = shallowMount,
@@ -34,7 +41,10 @@ describe('graph component', () => {
...props,
},
data() {
- return { ...data };
+ return {
+ ...defaultData,
+ ...data,
+ };
},
provide: {
dataMethod: GRAPHQL,
diff --git a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
index 54593c527cb..202365ecd35 100644
--- a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
@@ -1,11 +1,11 @@
+import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import { shallowMount } from '@vue/test-utils';
-import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import createMockApollo from 'helpers/mock_apollo_helper';
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
-import PipelineGraphWrapper from '~/pipelines/components/graph/graph_component_wrapper.vue';
import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
+import PipelineGraphWrapper from '~/pipelines/components/graph/graph_component_wrapper.vue';
import { mockPipelineResponse } from './mock_data';
const defaultProvide = {
diff --git a/spec/frontend/pipelines/graph/job_name_component_spec.js b/spec/frontend/pipelines/graph/job_name_component_spec.js
index f0aa646b8d7..658b5be87d4 100644
--- a/spec/frontend/pipelines/graph/job_name_component_spec.js
+++ b/spec/frontend/pipelines/graph/job_name_component_spec.js
@@ -1,7 +1,6 @@
import { mount } from '@vue/test-utils';
-import ciIcon from '~/vue_shared/components/ci_icon.vue';
-
import jobNameComponent from '~/pipelines/components/graph/job_name_component.vue';
+import ciIcon from '~/vue_shared/components/ci_icon.vue';
describe('job name component', () => {
let wrapper;
diff --git a/spec/frontend/pipelines/graph/linked_pipeline_spec.js b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
index fb005d628a9..96f2cd1e371 100644
--- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
@@ -1,9 +1,10 @@
-import { mount } from '@vue/test-utils';
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
+import { UPSTREAM, DOWNSTREAM } from '~/pipelines/components/graph/constants';
import LinkedPipelineComponent from '~/pipelines/components/graph/linked_pipeline.vue';
import CiStatus from '~/vue_shared/components/ci_icon.vue';
import mockData from './linked_pipelines_mock_data';
-import { UPSTREAM, DOWNSTREAM } from '~/pipelines/components/graph/constants';
const mockPipeline = mockData.triggered[0];
const validTriggeredPipelineId = mockPipeline.project.id;
@@ -212,11 +213,11 @@ describe('Linked pipeline', () => {
expect(wrapper.emitted().pipelineClicked).toBeTruthy();
});
- it('should emit `bv::hide::tooltip` to close the tooltip', () => {
+ it(`should emit ${BV_HIDE_TOOLTIP} to close the tooltip`, () => {
jest.spyOn(wrapper.vm.$root, '$emit');
findButton().trigger('click');
- expect(wrapper.vm.$root.$emit.mock.calls[0]).toEqual(['bv::hide::tooltip']);
+ expect(wrapper.vm.$root.$emit.mock.calls[0]).toEqual([BV_HIDE_TOOLTIP]);
});
it('should emit downstreamHovered with job name on mouseover', () => {
diff --git a/spec/frontend/pipelines/graph/linked_pipelines_column_legacy_spec.js b/spec/frontend/pipelines/graph/linked_pipelines_column_legacy_spec.js
index b6c700c65d2..200e3f48401 100644
--- a/spec/frontend/pipelines/graph/linked_pipelines_column_legacy_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipelines_column_legacy_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
-import LinkedPipelinesColumnLegacy from '~/pipelines/components/graph/linked_pipelines_column_legacy.vue';
-import LinkedPipeline from '~/pipelines/components/graph/linked_pipeline.vue';
import { UPSTREAM } from '~/pipelines/components/graph/constants';
+import LinkedPipeline from '~/pipelines/components/graph/linked_pipeline.vue';
+import LinkedPipelinesColumnLegacy from '~/pipelines/components/graph/linked_pipelines_column_legacy.vue';
import mockData from './linked_pipelines_mock_data';
describe('Linked Pipelines Column', () => {
diff --git a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
index 6db152f2607..8f01accccc1 100644
--- a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
@@ -1,11 +1,11 @@
-import VueApollo from 'vue-apollo';
import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
+import { DOWNSTREAM, GRAPHQL, UPSTREAM } from '~/pipelines/components/graph/constants';
import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
-import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue';
import LinkedPipeline from '~/pipelines/components/graph/linked_pipeline.vue';
-import { DOWNSTREAM, GRAPHQL } from '~/pipelines/components/graph/constants';
+import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue';
import { LOAD_FAILURE } from '~/pipelines/constants';
import {
mockPipelineResponse,
@@ -17,7 +17,7 @@ const processedPipeline = pipelineWithUpstreamDownstream(mockPipelineResponse);
describe('Linked Pipelines Column', () => {
const defaultProps = {
- columnTitle: 'Upstream',
+ columnTitle: 'Downstream',
linkedPipelines: processedPipeline.downstream,
type: DOWNSTREAM,
};
@@ -45,14 +45,15 @@ describe('Linked Pipelines Column', () => {
});
};
- const createComponentWithApollo = (
+ const createComponentWithApollo = ({
mountFn = shallowMount,
getPipelineDetailsHandler = jest.fn().mockResolvedValue(wrappedPipelineReturn),
- ) => {
+ props = {},
+ } = {}) => {
const requestHandlers = [[getPipelineDetails, getPipelineDetailsHandler]];
const apolloProvider = createMockApollo(requestHandlers);
- createComponent({ apolloProvider, mountFn });
+ createComponent({ apolloProvider, mountFn, props });
};
afterEach(() => {
@@ -86,34 +87,90 @@ describe('Linked Pipelines Column', () => {
await wrapper.vm.$nextTick();
};
- describe('when successful', () => {
- beforeEach(() => {
- createComponentWithApollo(mount);
+ describe('downstream', () => {
+ describe('when successful', () => {
+ beforeEach(() => {
+ createComponentWithApollo({ mountFn: mount });
+ });
+
+ it('toggles the pipeline visibility', async () => {
+ expect(findPipelineGraph().exists()).toBe(false);
+ await clickExpandButtonAndAwaitTimers();
+ expect(findPipelineGraph().exists()).toBe(true);
+ await clickExpandButton();
+ expect(findPipelineGraph().exists()).toBe(false);
+ });
});
- it('toggles the pipeline visibility', async () => {
- expect(findPipelineGraph().exists()).toBe(false);
- await clickExpandButtonAndAwaitTimers();
- expect(findPipelineGraph().exists()).toBe(true);
- await clickExpandButton();
- expect(findPipelineGraph().exists()).toBe(false);
+ describe('on error', () => {
+ beforeEach(() => {
+ createComponentWithApollo({
+ mountFn: mount,
+ getPipelineDetailsHandler: jest.fn().mockRejectedValue(new Error('GraphQL error')),
+ });
+ });
+
+ it('emits the error', async () => {
+ await clickExpandButton();
+ expect(wrapper.emitted().error).toEqual([[LOAD_FAILURE]]);
+ });
+
+ it('does not show the pipeline', async () => {
+ expect(findPipelineGraph().exists()).toBe(false);
+ await clickExpandButtonAndAwaitTimers();
+ expect(findPipelineGraph().exists()).toBe(false);
+ });
});
});
- describe('on error', () => {
- beforeEach(() => {
- createComponentWithApollo(mount, jest.fn().mockRejectedValue(new Error('GraphQL error')));
- });
-
- it('emits the error', async () => {
- await clickExpandButton();
- expect(wrapper.emitted().error).toEqual([[LOAD_FAILURE]]);
+ describe('upstream', () => {
+ const upstreamProps = {
+ columnTitle: 'Upstream',
+ /*
+ Because the IDs need to match to work, rather
+ than make new mock data, we are representing
+ the upstream pipeline with the downstream data.
+ */
+ linkedPipelines: processedPipeline.downstream,
+ type: UPSTREAM,
+ };
+
+ describe('when successful', () => {
+ beforeEach(() => {
+ createComponentWithApollo({
+ mountFn: mount,
+ props: upstreamProps,
+ });
+ });
+
+ it('toggles the pipeline visibility', async () => {
+ expect(findPipelineGraph().exists()).toBe(false);
+ await clickExpandButtonAndAwaitTimers();
+ expect(findPipelineGraph().exists()).toBe(true);
+ await clickExpandButton();
+ expect(findPipelineGraph().exists()).toBe(false);
+ });
});
- it('does not show the pipeline', async () => {
- expect(findPipelineGraph().exists()).toBe(false);
- await clickExpandButtonAndAwaitTimers();
- expect(findPipelineGraph().exists()).toBe(false);
+ describe('on error', () => {
+ beforeEach(() => {
+ createComponentWithApollo({
+ mountFn: mount,
+ getPipelineDetailsHandler: jest.fn().mockRejectedValue(new Error('GraphQL error')),
+ props: upstreamProps,
+ });
+ });
+
+ it('emits the error', async () => {
+ await clickExpandButton();
+ expect(wrapper.emitted().error).toEqual([[LOAD_FAILURE]]);
+ });
+
+ it('does not show the pipeline', async () => {
+ expect(findPipelineGraph().exists()).toBe(false);
+ await clickExpandButtonAndAwaitTimers();
+ expect(findPipelineGraph().exists()).toBe(false);
+ });
});
});
});
diff --git a/spec/frontend/pipelines/graph/stage_column_component_spec.js b/spec/frontend/pipelines/graph/stage_column_component_spec.js
index 202e25ccda3..16dc70a63a5 100644
--- a/spec/frontend/pipelines/graph/stage_column_component_spec.js
+++ b/spec/frontend/pipelines/graph/stage_column_component_spec.js
@@ -68,6 +68,10 @@ describe('stage column component', () => {
it('should render the provided groups', () => {
expect(findAllStageColumnGroups().length).toBe(mockGroups.length);
});
+
+ it('should emit updateMeasurements event on mount', () => {
+ expect(wrapper.emitted().updateMeasurements).toHaveLength(1);
+ });
});
describe('when job notifies action is complete', () => {
diff --git a/spec/frontend/pipelines/graph_shared/__snapshots__/links_inner_spec.js.snap b/spec/frontend/pipelines/graph_shared/__snapshots__/links_inner_spec.js.snap
new file mode 100644
index 00000000000..cf2b66dea5f
--- /dev/null
+++ b/spec/frontend/pipelines/graph_shared/__snapshots__/links_inner_spec.js.snap
@@ -0,0 +1,23 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Links Inner component with a large number of needs matches snapshot and has expected path 1`] = `
+"<div class=\\"gl-display-flex gl-relative\\"><svg id=\\"link-svg\\" viewBox=\\"0,0,1019,445\\" width=\\"1019px\\" height=\\"445px\\" class=\\"gl-absolute gl-pointer-events-none\\">
+ <path d=\\"M202,118L42,118C72,118,72,138,102,138\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
+ <path d=\\"M202,118L52,118C82,118,82,148,112,148\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
+ <path d=\\"M222,138L62,138C92,138,92,158,122,158\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
+ <path d=\\"M212,128L72,128C102,128,102,168,132,168\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
+ <path d=\\"M232,148L82,148C112,148,112,178,142,178\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
+ </svg> </div>"
+`;
+
+exports[`Links Inner component with a parallel need matches snapshot and has expected path 1`] = `
+"<div class=\\"gl-display-flex gl-relative\\"><svg id=\\"link-svg\\" viewBox=\\"0,0,1019,445\\" width=\\"1019px\\" height=\\"445px\\" class=\\"gl-absolute gl-pointer-events-none\\">
+ <path d=\\"M192,108L22,108C52,108,52,118,82,118\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
+ </svg> </div>"
+`;
+
+exports[`Links Inner component with one need matches snapshot and has expected path 1`] = `
+"<div class=\\"gl-display-flex gl-relative\\"><svg id=\\"link-svg\\" viewBox=\\"0,0,1019,445\\" width=\\"1019px\\" height=\\"445px\\" class=\\"gl-absolute gl-pointer-events-none\\">
+ <path d=\\"M202,118L42,118C72,118,72,138,102,138\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
+ </svg> </div>"
+`;
diff --git a/spec/frontend/pipelines/graph_shared/links_inner_spec.js b/spec/frontend/pipelines/graph_shared/links_inner_spec.js
new file mode 100644
index 00000000000..6cabe2bc8a7
--- /dev/null
+++ b/spec/frontend/pipelines/graph_shared/links_inner_spec.js
@@ -0,0 +1,197 @@
+import { shallowMount } from '@vue/test-utils';
+import { setHTMLFixture } from 'helpers/fixtures';
+import LinksInner from '~/pipelines/components/graph_shared/links_inner.vue';
+import { createJobsHash } from '~/pipelines/utils';
+import {
+ jobRect,
+ largePipelineData,
+ parallelNeedData,
+ pipelineData,
+ pipelineDataWithNoNeeds,
+ rootRect,
+} from '../pipeline_graph/mock_data';
+
+describe('Links Inner component', () => {
+ const containerId = 'pipeline-graph-container';
+ const defaultProps = {
+ containerId,
+ containerMeasurements: { width: 1019, height: 445 },
+ pipelineId: 1,
+ pipelineData: [],
+ };
+ let wrapper;
+
+ const createComponent = (props) => {
+ wrapper = shallowMount(LinksInner, {
+ propsData: { ...defaultProps, ...props },
+ });
+ };
+
+ const findLinkSvg = () => wrapper.find('#link-svg');
+ const findAllLinksPath = () => findLinkSvg().findAll('path');
+
+ // We create fixture so that each job has an empty div that represent
+ // the JobPill in the DOM. Each `JobPill` would have different coordinates,
+ // so we increment their coordinates on each iteration to simulat different positions.
+ const setFixtures = ({ stages }) => {
+ const jobs = createJobsHash(stages);
+ const arrayOfJobs = Object.keys(jobs);
+
+ const linksHtmlElements = arrayOfJobs.map((job) => {
+ return `<div id=${job}-${defaultProps.pipelineId} />`;
+ });
+
+ setHTMLFixture(`<div id="${containerId}">${linksHtmlElements.join(' ')}</div>`);
+
+ // We are mocking the clientRect data of each job and the container ID.
+ jest
+ .spyOn(document.getElementById(containerId), 'getBoundingClientRect')
+ .mockImplementation(() => rootRect);
+
+ arrayOfJobs.forEach((job, index) => {
+ jest
+ .spyOn(
+ document.getElementById(`${job}-${defaultProps.pipelineId}`),
+ 'getBoundingClientRect',
+ )
+ .mockImplementation(() => {
+ const newValue = 10 * index;
+ const { left, right, top, bottom, x, y } = jobRect;
+ return {
+ ...jobRect,
+ left: left + newValue,
+ right: right + newValue,
+ top: top + newValue,
+ bottom: bottom + newValue,
+ x: x + newValue,
+ y: y + newValue,
+ };
+ });
+ });
+ };
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('basic SVG creation', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders an SVG of the right size', () => {
+ expect(findLinkSvg().exists()).toBe(true);
+ expect(findLinkSvg().attributes('width')).toBe(
+ `${defaultProps.containerMeasurements.width}px`,
+ );
+ expect(findLinkSvg().attributes('height')).toBe(
+ `${defaultProps.containerMeasurements.height}px`,
+ );
+ });
+ });
+
+ describe('no pipeline data', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the component', () => {
+ expect(findLinkSvg().exists()).toBe(true);
+ expect(findAllLinksPath()).toHaveLength(0);
+ });
+ });
+
+ describe('pipeline data with no needs', () => {
+ beforeEach(() => {
+ createComponent({ pipelineData: pipelineDataWithNoNeeds.stages });
+ });
+
+ it('renders no links', () => {
+ expect(findLinkSvg().exists()).toBe(true);
+ expect(findAllLinksPath()).toHaveLength(0);
+ });
+ });
+
+ describe('with one need', () => {
+ beforeEach(() => {
+ setFixtures(pipelineData);
+ createComponent({ pipelineData: pipelineData.stages });
+ });
+
+ it('renders one link', () => {
+ expect(findAllLinksPath()).toHaveLength(1);
+ });
+
+ it('path does not contain NaN values', () => {
+ expect(wrapper.html()).not.toContain('NaN');
+ });
+
+ it('matches snapshot and has expected path', () => {
+ expect(wrapper.html()).toMatchSnapshot();
+ });
+ });
+
+ describe('with a parallel need', () => {
+ beforeEach(() => {
+ setFixtures(parallelNeedData);
+ createComponent({ pipelineData: parallelNeedData.stages });
+ });
+
+ it('renders only one link for all the same parallel jobs', () => {
+ expect(findAllLinksPath()).toHaveLength(1);
+ });
+
+ it('path does not contain NaN values', () => {
+ expect(wrapper.html()).not.toContain('NaN');
+ });
+
+ it('matches snapshot and has expected path', () => {
+ expect(wrapper.html()).toMatchSnapshot();
+ });
+ });
+
+ describe('with a large number of needs', () => {
+ beforeEach(() => {
+ setFixtures(largePipelineData);
+ createComponent({ pipelineData: largePipelineData.stages });
+ });
+
+ it('renders the correct number of links', () => {
+ expect(findAllLinksPath()).toHaveLength(5);
+ });
+
+ it('path does not contain NaN values', () => {
+ expect(wrapper.html()).not.toContain('NaN');
+ });
+
+ it('matches snapshot and has expected path', () => {
+ expect(wrapper.html()).toMatchSnapshot();
+ });
+ });
+
+ describe('interactions', () => {
+ beforeEach(() => {
+ setFixtures(largePipelineData);
+ createComponent({ pipelineData: largePipelineData.stages });
+ });
+
+ it('highlight needs on hover', async () => {
+ const firstLink = findAllLinksPath().at(0);
+
+ const defaultColorClass = 'gl-stroke-gray-200';
+ const hoverColorClass = 'gl-stroke-blue-400';
+
+ expect(firstLink.classes(defaultColorClass)).toBe(true);
+ expect(firstLink.classes(hoverColorClass)).toBe(false);
+
+ // Because there is a watcher, we need to set the props after the component
+ // has mounted.
+ await wrapper.setProps({ highlightedJob: 'test_1' });
+
+ expect(firstLink.classes(defaultColorClass)).toBe(false);
+ expect(firstLink.classes(hoverColorClass)).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/shared/links_layer_spec.js b/spec/frontend/pipelines/graph_shared/links_layer_spec.js
index 9ef5233dbce..0ff8583fbff 100644
--- a/spec/frontend/pipelines/shared/links_layer_spec.js
+++ b/spec/frontend/pipelines/graph_shared/links_layer_spec.js
@@ -1,7 +1,7 @@
-import { mount, shallowMount } from '@vue/test-utils';
import { GlAlert, GlButton } from '@gitlab/ui';
-import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
+import { mount, shallowMount } from '@vue/test-utils';
import LinksInner from '~/pipelines/components/graph_shared/links_inner.vue';
+import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
import { generateResponse, mockPipelineResponse } from '../graph/mock_data';
describe('links layer component', () => {
diff --git a/spec/frontend/pipelines/header_component_spec.js b/spec/frontend/pipelines/header_component_spec.js
index 03e385e3cc8..57d846c53c8 100644
--- a/spec/frontend/pipelines/header_component_spec.js
+++ b/spec/frontend/pipelines/header_component_spec.js
@@ -1,15 +1,15 @@
-import { shallowMount } from '@vue/test-utils';
import { GlModal, GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import HeaderComponent from '~/pipelines/components/header_component.vue';
+import cancelPipelineMutation from '~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql';
+import deletePipelineMutation from '~/pipelines/graphql/mutations/delete_pipeline.mutation.graphql';
+import retryPipelineMutation from '~/pipelines/graphql/mutations/retry_pipeline.mutation.graphql';
import {
mockCancelledPipelineHeader,
mockFailedPipelineHeader,
mockRunningPipelineHeader,
mockSuccessfulPipelineHeader,
} from './mock_data';
-import HeaderComponent from '~/pipelines/components/header_component.vue';
-import deletePipelineMutation from '~/pipelines/graphql/mutations/delete_pipeline.mutation.graphql';
-import retryPipelineMutation from '~/pipelines/graphql/mutations/retry_pipeline.mutation.graphql';
-import cancelPipelineMutation from '~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql';
describe('Pipeline details header', () => {
let wrapper;
diff --git a/spec/frontend/pipelines/legacy_header_component_spec.js b/spec/frontend/pipelines/legacy_header_component_spec.js
deleted file mode 100644
index fb7feb8898a..00000000000
--- a/spec/frontend/pipelines/legacy_header_component_spec.js
+++ /dev/null
@@ -1,116 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlModal } from '@gitlab/ui';
-import LegacyHeaderComponent from '~/pipelines/components/legacy_header_component.vue';
-import CiHeader from '~/vue_shared/components/header_ci_component.vue';
-import eventHub from '~/pipelines/event_hub';
-
-describe('Pipeline details header', () => {
- let wrapper;
- let glModalDirective;
-
- const threeWeeksAgo = new Date();
- threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21);
-
- const findDeleteModal = () => wrapper.find(GlModal);
-
- const defaultProps = {
- pipeline: {
- details: {
- status: {
- group: 'failed',
- icon: 'status_failed',
- label: 'failed',
- text: 'failed',
- details_path: 'path',
- },
- },
- id: 123,
- created_at: threeWeeksAgo.toISOString(),
- user: {
- web_url: 'path',
- name: 'Foo',
- username: 'foobar',
- email: 'foo@bar.com',
- avatar_url: 'link',
- },
- retry_path: 'retry',
- cancel_path: 'cancel',
- delete_path: 'delete',
- },
- isLoading: false,
- };
-
- const createComponent = (props = {}) => {
- glModalDirective = jest.fn();
-
- wrapper = shallowMount(LegacyHeaderComponent, {
- propsData: {
- ...props,
- },
- directives: {
- glModal: {
- bind(el, { value }) {
- glModalDirective(value);
- },
- },
- },
- });
- };
-
- beforeEach(() => {
- jest.spyOn(eventHub, '$emit');
-
- createComponent(defaultProps);
- });
-
- afterEach(() => {
- eventHub.$off();
-
- wrapper.destroy();
- wrapper = null;
- });
-
- it('should render provided pipeline info', () => {
- expect(wrapper.find(CiHeader).props()).toMatchObject({
- status: defaultProps.pipeline.details.status,
- itemId: defaultProps.pipeline.id,
- time: defaultProps.pipeline.created_at,
- user: defaultProps.pipeline.user,
- });
- });
-
- describe('action buttons', () => {
- it('should not trigger eventHub when nothing happens', () => {
- expect(eventHub.$emit).not.toHaveBeenCalled();
- });
-
- it('should call postAction when retry button action is clicked', () => {
- wrapper.find('[data-testid="retryButton"]').vm.$emit('click');
-
- expect(eventHub.$emit).toHaveBeenCalledWith('headerPostAction', 'retry');
- });
-
- it('should call postAction when cancel button action is clicked', () => {
- wrapper.find('[data-testid="cancelPipeline"]').vm.$emit('click');
-
- expect(eventHub.$emit).toHaveBeenCalledWith('headerPostAction', 'cancel');
- });
-
- it('does not show delete modal', () => {
- expect(findDeleteModal()).not.toBeVisible();
- });
-
- describe('when delete button action is clicked', () => {
- it('displays delete modal', () => {
- expect(findDeleteModal().props('modalId')).toBe(wrapper.vm.$options.DELETE_MODAL_ID);
- expect(glModalDirective).toHaveBeenCalledWith(wrapper.vm.$options.DELETE_MODAL_ID);
- });
-
- it('should call delete when modal is submitted', () => {
- findDeleteModal().vm.$emit('ok');
-
- expect(eventHub.$emit).toHaveBeenCalledWith('headerDeleteAction', 'delete');
- });
- });
- });
-});
diff --git a/spec/frontend/pipelines/pipeline_graph/mock_data.js b/spec/frontend/pipelines/pipeline_graph/mock_data.js
index 7d1a7a79c7f..339aac9f349 100644
--- a/spec/frontend/pipelines/pipeline_graph/mock_data.js
+++ b/spec/frontend/pipelines/pipeline_graph/mock_data.js
@@ -1,5 +1,3 @@
-import { createUniqueLinkId } from '~/pipelines/components/graph_shared/drawing_utils';
-
export const yamlString = `stages:
- empty
- build
@@ -41,10 +39,28 @@ deploy_a:
script: echo hello
`;
-const jobId1 = createUniqueLinkId('build', 'build_1');
-const jobId2 = createUniqueLinkId('test', 'test_1');
-const jobId3 = createUniqueLinkId('test', 'test_2');
-const jobId4 = createUniqueLinkId('deploy', 'deploy_1');
+export const pipelineDataWithNoNeeds = {
+ stages: [
+ {
+ name: 'build',
+ groups: [
+ {
+ name: 'build_1',
+ jobs: [{ script: 'echo hello', stage: 'build' }],
+ },
+ ],
+ },
+ {
+ name: 'test',
+ groups: [
+ {
+ name: 'test_1',
+ jobs: [{ script: 'yarn test', stage: 'test' }],
+ },
+ ],
+ },
+ ],
+};
export const pipelineData = {
stages: [
@@ -54,7 +70,6 @@ export const pipelineData = {
{
name: 'build_1',
jobs: [{ script: 'echo hello', stage: 'build' }],
- id: jobId1,
},
],
},
@@ -64,12 +79,10 @@ export const pipelineData = {
{
name: 'test_1',
jobs: [{ script: 'yarn test', stage: 'test' }],
- id: jobId2,
},
{
name: 'test_2',
jobs: [{ script: 'yarn karma', stage: 'test' }],
- id: jobId3,
},
],
},
@@ -79,7 +92,86 @@ export const pipelineData = {
{
name: 'deploy_1',
jobs: [{ script: 'yarn magick', stage: 'deploy', needs: ['test_1'] }],
- id: jobId4,
+ },
+ ],
+ },
+ ],
+};
+
+export const parallelNeedData = {
+ stages: [
+ {
+ name: 'build',
+ groups: [
+ {
+ name: 'build_1',
+ parallel: 3,
+ jobs: [
+ { script: 'echo hello', stage: 'build', name: 'build_1 1/3' },
+ { script: 'echo hello', stage: 'build', name: 'build_1 2/3' },
+ { script: 'echo hello', stage: 'build', name: 'build_1 3/3' },
+ ],
+ },
+ ],
+ },
+ {
+ name: 'test',
+ groups: [
+ {
+ name: 'test_1',
+ jobs: [{ script: 'yarn test', stage: 'test', needs: ['build_1'] }],
+ },
+ ],
+ },
+ ],
+};
+
+export const largePipelineData = {
+ stages: [
+ {
+ name: 'build',
+ groups: [
+ {
+ name: 'build_1',
+ jobs: [{ script: 'echo hello', stage: 'build' }],
+ },
+ {
+ name: 'build_2',
+ jobs: [{ script: 'echo hello', stage: 'build' }],
+ },
+ {
+ name: 'build_3',
+ jobs: [{ script: 'echo hello', stage: 'build' }],
+ },
+ ],
+ },
+ {
+ name: 'test',
+ groups: [
+ {
+ name: 'test_1',
+ jobs: [{ script: 'yarn test', stage: 'test', needs: ['build_2'] }],
+ },
+ {
+ name: 'test_2',
+ jobs: [{ script: 'yarn karma', stage: 'test', needs: ['build_2'] }],
+ },
+ ],
+ },
+ {
+ name: 'deploy',
+ groups: [
+ {
+ name: 'deploy_1',
+ jobs: [{ script: 'yarn magick', stage: 'deploy', needs: ['test_1'] }],
+ },
+ {
+ name: 'deploy_2',
+ jobs: [{ script: 'yarn magick', stage: 'deploy', needs: ['build_3'] }],
+ },
+ {
+ name: 'deploy_3',
+ jobs: [{ script: 'yarn magick', stage: 'deploy', needs: ['test_2'] }],
},
],
},
@@ -94,9 +186,30 @@ export const singleStageData = {
{
name: 'build_1',
jobs: [{ script: 'echo hello', stage: 'build' }],
- id: jobId1,
},
],
},
],
};
+
+export const rootRect = {
+ bottom: 463,
+ height: 271,
+ left: 236,
+ right: 1252,
+ top: 192,
+ width: 1016,
+ x: 236,
+ y: 192,
+};
+
+export const jobRect = {
+ bottom: 312,
+ height: 24,
+ left: 308,
+ right: 428,
+ top: 288,
+ width: 120,
+ x: 308,
+ y: 288,
+};
diff --git a/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js b/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
index b6b0a964383..718667fcc73 100644
--- a/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
+++ b/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
@@ -1,11 +1,11 @@
-import { shallowMount } from '@vue/test-utils';
import { GlAlert } from '@gitlab/ui';
-import { pipelineData, singleStageData } from './mock_data';
+import { shallowMount } from '@vue/test-utils';
import { CI_CONFIG_STATUS_INVALID, CI_CONFIG_STATUS_VALID } from '~/pipeline_editor/constants';
-import { DRAW_FAILURE, EMPTY_PIPELINE_DATA, INVALID_CI_CONFIG } from '~/pipelines/constants';
+import JobPill from '~/pipelines/components/pipeline_graph/job_pill.vue';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
import StagePill from '~/pipelines/components/pipeline_graph/stage_pill.vue';
-import JobPill from '~/pipelines/components/pipeline_graph/job_pill.vue';
+import { DRAW_FAILURE, EMPTY_PIPELINE_DATA, INVALID_CI_CONFIG } from '~/pipelines/constants';
+import { pipelineData, singleStageData } from './mock_data';
describe('pipeline graph component', () => {
const defaultProps = { pipelineData };
diff --git a/spec/frontend/pipelines/pipeline_triggerer_spec.js b/spec/frontend/pipelines/pipeline_triggerer_spec.js
index ad8136890e6..467a97d95c7 100644
--- a/spec/frontend/pipelines/pipeline_triggerer_spec.js
+++ b/spec/frontend/pipelines/pipeline_triggerer_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import pipelineTriggerer from '~/pipelines/components/pipelines_list/pipeline_triggerer.vue';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
describe('Pipelines Triggerer', () => {
let wrapper;
diff --git a/spec/frontend/pipelines/pipeline_url_spec.js b/spec/frontend/pipelines/pipeline_url_spec.js
index 47315bd42e6..44c9def99cc 100644
--- a/spec/frontend/pipelines/pipeline_url_spec.js
+++ b/spec/frontend/pipelines/pipeline_url_spec.js
@@ -1,6 +1,6 @@
+import { shallowMount } from '@vue/test-utils';
import $ from 'jquery';
import { trimText } from 'helpers/text_helper';
-import { shallowMount } from '@vue/test-utils';
import PipelineUrlComponent from '~/pipelines/components/pipelines_list/pipeline_url.vue';
$.fn.popover = () => {};
@@ -17,6 +17,7 @@ describe('Pipeline Url Component', () => {
const findStuckTag = () => wrapper.find('[data-testid="pipeline-url-stuck"]');
const findDetachedTag = () => wrapper.find('[data-testid="pipeline-url-detached"]');
const findForkTag = () => wrapper.find('[data-testid="pipeline-url-fork"]');
+ const findTrainTag = () => wrapper.find('[data-testid="pipeline-url-train"]');
const defaultProps = {
pipeline: {
@@ -141,6 +142,7 @@ describe('Pipeline Url Component', () => {
expect(findScheduledTag().exists()).toBe(true);
expect(findScheduledTag().text()).toContain('Scheduled');
});
+
it('should render the fork badge when the pipeline was run in a fork', () => {
createComponent({
pipeline: {
@@ -152,4 +154,28 @@ describe('Pipeline Url Component', () => {
expect(findForkTag().exists()).toBe(true);
expect(findForkTag().text()).toBe('fork');
});
+
+ it('should render the train badge when the pipeline is a merge train pipeline', () => {
+ createComponent({
+ pipeline: {
+ flags: {
+ merge_train_pipeline: true,
+ },
+ },
+ });
+
+ expect(findTrainTag().text()).toContain('train');
+ });
+
+ it('should not render the train badge when the pipeline is not a merge train pipeline', () => {
+ createComponent({
+ pipeline: {
+ flags: {
+ merge_train_pipeline: false,
+ },
+ },
+ });
+
+ expect(findTrainTag().exists()).toBe(false);
+ });
});
diff --git a/spec/frontend/pipelines/pipelines_actions_spec.js b/spec/frontend/pipelines/pipelines_actions_spec.js
index 69c1b7ce43d..1e6c9e50a7e 100644
--- a/spec/frontend/pipelines/pipelines_actions_spec.js
+++ b/spec/frontend/pipelines/pipelines_actions_spec.js
@@ -1,25 +1,29 @@
-import { shallowMount } from '@vue/test-utils';
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { shallowMount, mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import { TEST_HOST } from 'spec/test_constants';
-import { GlButton } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
+import { TEST_HOST } from 'spec/test_constants';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import PipelinesActions from '~/pipelines/components/pipelines_list/pipelines_actions.vue';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
+jest.mock('~/flash');
+
describe('Pipelines Actions dropdown', () => {
let wrapper;
let mock;
- const createComponent = (actions = []) => {
- wrapper = shallowMount(PipelinesActions, {
+ const createComponent = (props, mountFn = shallowMount) => {
+ wrapper = mountFn(PipelinesActions, {
propsData: {
- actions,
+ ...props,
},
});
};
- const findAllDropdownItems = () => wrapper.findAll(GlButton);
+ const findDropdown = () => wrapper.find(GlDropdown);
+ const findAllDropdownItems = () => wrapper.findAll(GlDropdownItem);
const findAllCountdowns = () => wrapper.findAll(GlCountdown);
beforeEach(() => {
@@ -47,7 +51,7 @@ describe('Pipelines Actions dropdown', () => {
];
beforeEach(() => {
- createComponent(mockActions);
+ createComponent({ actions: mockActions });
});
it('renders a dropdown with the provided actions', () => {
@@ -59,16 +63,33 @@ describe('Pipelines Actions dropdown', () => {
});
describe('on click', () => {
- it('makes a request and toggles the loading state', () => {
+ beforeEach(() => {
+ createComponent({ actions: mockActions }, mount);
+ });
+
+ it('makes a request and toggles the loading state', async () => {
mock.onPost(mockActions.path).reply(200);
- wrapper.find(GlButton).vm.$emit('click');
+ findAllDropdownItems().at(0).vm.$emit('click');
+
+ await wrapper.vm.$nextTick();
+ expect(findDropdown().props('loading')).toBe(true);
+
+ await waitForPromises();
+ expect(findDropdown().props('loading')).toBe(false);
+ });
+
+ it('makes a failed request and toggles the loading state', async () => {
+ mock.onPost(mockActions.path).reply(500);
- expect(wrapper.vm.isLoading).toBe(true);
+ findAllDropdownItems().at(0).vm.$emit('click');
- return waitForPromises().then(() => {
- expect(wrapper.vm.isLoading).toBe(false);
- });
+ await wrapper.vm.$nextTick();
+ expect(findDropdown().props('loading')).toBe(true);
+
+ await waitForPromises();
+ expect(findDropdown().props('loading')).toBe(false);
+ expect(createFlash).toHaveBeenCalledTimes(1);
});
});
});
@@ -89,10 +110,10 @@ describe('Pipelines Actions dropdown', () => {
beforeEach(() => {
jest.spyOn(Date, 'now').mockImplementation(() => new Date('2063-04-04T00:42:00Z').getTime());
- createComponent([scheduledJobAction, expiredJobAction]);
+ createComponent({ actions: [scheduledJobAction, expiredJobAction] });
});
- it('makes post request after confirming', () => {
+ it('makes post request after confirming', async () => {
mock.onPost(scheduledJobAction.path).reply(200);
jest.spyOn(window, 'confirm').mockReturnValue(true);
@@ -100,19 +121,22 @@ describe('Pipelines Actions dropdown', () => {
expect(window.confirm).toHaveBeenCalled();
- return waitForPromises().then(() => {
- expect(mock.history.post.length).toBe(1);
- });
+ await waitForPromises();
+
+ expect(mock.history.post).toHaveLength(1);
});
- it('does not make post request if confirmation is cancelled', () => {
+ it('does not make post request if confirmation is cancelled', async () => {
mock.onPost(scheduledJobAction.path).reply(200);
jest.spyOn(window, 'confirm').mockReturnValue(false);
findAllDropdownItems().at(0).vm.$emit('click');
expect(window.confirm).toHaveBeenCalled();
- expect(mock.history.post.length).toBe(0);
+
+ await waitForPromises();
+
+ expect(mock.history.post).toHaveLength(0);
});
it('displays the remaining time in the dropdown', () => {
diff --git a/spec/frontend/pipelines/pipelines_artifacts_spec.js b/spec/frontend/pipelines/pipelines_artifacts_spec.js
index 4f4c15fd4cc..f077833ae16 100644
--- a/spec/frontend/pipelines/pipelines_artifacts_spec.js
+++ b/spec/frontend/pipelines/pipelines_artifacts_spec.js
@@ -1,5 +1,5 @@
-import { mount } from '@vue/test-utils';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import PipelineArtifacts from '~/pipelines/components/pipelines_list/pipelines_artifacts.vue';
describe('Pipelines Artifacts dropdown', () => {
diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js
index 5d82669b0b8..811303a5624 100644
--- a/spec/frontend/pipelines/pipelines_spec.js
+++ b/spec/frontend/pipelines/pipelines_spec.js
@@ -1,49 +1,50 @@
-import { nextTick } from 'vue';
+import { GlFilteredSearch, GlButton, GlLoadingIcon, GlPagination } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import { chunk } from 'lodash';
+import { nextTick } from 'vue';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { GlFilteredSearch, GlButton, GlLoadingIcon } from '@gitlab/ui';
import Api from '~/api';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
-import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
-
-import NavigationControls from '~/pipelines/components/pipelines_list/nav_controls.vue';
-import EmptyState from '~/pipelines/components/pipelines_list/empty_state.vue';
import BlankState from '~/pipelines/components/pipelines_list/blank_state.vue';
-import PipelinesTableComponent from '~/pipelines/components/pipelines_list/pipelines_table.vue';
-
+import EmptyState from '~/pipelines/components/pipelines_list/empty_state.vue';
+import NavigationControls from '~/pipelines/components/pipelines_list/nav_controls.vue';
import PipelinesComponent from '~/pipelines/components/pipelines_list/pipelines.vue';
+import PipelinesTableComponent from '~/pipelines/components/pipelines_list/pipelines_table.vue';
+import { RAW_TEXT_WARNING } from '~/pipelines/constants';
import Store from '~/pipelines/stores/pipelines_store';
+import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
+import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
+
import { pipelineWithStages, stageReply, users, mockSearch, branches } from './mock_data';
-import { RAW_TEXT_WARNING } from '~/pipelines/constants';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
jest.mock('~/flash');
-describe('Pipelines', () => {
- const jsonFixtureName = 'pipelines/pipelines.json';
+const mockProjectPath = 'twitter/flight';
+const mockProjectId = '21';
+const mockPipelinesEndpoint = `/${mockProjectPath}/pipelines.json`;
+const mockPipelinesResponse = getJSONFixture('pipelines/pipelines.json');
+const mockPipelinesIds = mockPipelinesResponse.pipelines.map(({ id }) => id);
- preloadFixtures(jsonFixtureName);
-
- let pipelines;
+describe('Pipelines', () => {
let wrapper;
let mock;
+ let origWindowLocation;
const paths = {
- endpoint: 'twitter/flight/pipelines.json',
autoDevopsHelpPath: '/help/topics/autodevops/index.md',
helpPagePath: '/help/ci/quick_start/README',
emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg',
errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg',
noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg',
ciLintPath: '/ci/lint',
- resetCachePath: '/twitter/flight/settings/ci_cd/reset_cache',
- newPipelinePath: '/twitter/flight/pipelines/new',
+ resetCachePath: `${mockProjectPath}/settings/ci_cd/reset_cache`,
+ newPipelinePath: `${mockProjectPath}/pipelines/new`,
};
const noPermissions = {
- endpoint: 'twitter/flight/pipelines.json',
autoDevopsHelpPath: '/help/topics/autodevops/index.md',
helpPagePath: '/help/ci/quick_start/README',
emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg',
@@ -57,101 +58,140 @@ describe('Pipelines', () => {
...paths,
};
- const findFilteredSearch = () => wrapper.find(GlFilteredSearch);
- const findByTestId = (id) => wrapper.find(`[data-testid="${id}"]`);
- const findNavigationTabs = () => wrapper.find(NavigationTabs);
- const findNavigationControls = () => wrapper.find(NavigationControls);
- const findTab = (tab) => findByTestId(`pipelines-tab-${tab}`);
-
- const findRunPipelineButton = () => findByTestId('run-pipeline-button');
- const findCiLintButton = () => findByTestId('ci-lint-button');
- const findCleanCacheButton = () => findByTestId('clear-cache-button');
-
- const findEmptyState = () => wrapper.find(EmptyState);
- const findBlankState = () => wrapper.find(BlankState);
- const findStagesDropdown = () => wrapper.find('.js-builds-dropdown-button');
-
- const findTablePagination = () => wrapper.find(TablePagination);
+ const findFilteredSearch = () => wrapper.findComponent(GlFilteredSearch);
+ const findNavigationTabs = () => wrapper.findComponent(NavigationTabs);
+ const findNavigationControls = () => wrapper.findComponent(NavigationControls);
+ const findPipelinesTable = () => wrapper.findComponent(PipelinesTableComponent);
+ const findEmptyState = () => wrapper.findComponent(EmptyState);
+ const findBlankState = () => wrapper.findComponent(BlankState);
+ const findTablePagination = () => wrapper.findComponent(TablePagination);
+
+ const findTab = (tab) => wrapper.findByTestId(`pipelines-tab-${tab}`);
+ const findRunPipelineButton = () => wrapper.findByTestId('run-pipeline-button');
+ const findCiLintButton = () => wrapper.findByTestId('ci-lint-button');
+ const findCleanCacheButton = () => wrapper.findByTestId('clear-cache-button');
+ const findStagesDropdown = () => wrapper.findByTestId('mini-pipeline-graph-dropdown-toggle');
+ const findPipelineUrlLinks = () => wrapper.findAll('[data-testid="pipeline-url-link"]');
const createComponent = (props = defaultProps) => {
- wrapper = mount(PipelinesComponent, {
- propsData: {
- store: new Store(),
- projectId: '21',
- params: {},
- ...props,
- },
- });
+ wrapper = extendedWrapper(
+ mount(PipelinesComponent, {
+ propsData: {
+ store: new Store(),
+ projectId: mockProjectId,
+ endpoint: mockPipelinesEndpoint,
+ params: {},
+ ...props,
+ },
+ }),
+ );
};
- beforeEach(() => {
+ beforeAll(() => {
+ origWindowLocation = window.location;
delete window.location;
+ window.location = { search: '' };
+ });
+
+ afterAll(() => {
+ window.location = origWindowLocation;
});
beforeEach(() => {
- window.location = { search: '' };
mock = new MockAdapter(axios);
- pipelines = getJSONFixture(jsonFixtureName);
+ jest.spyOn(window.history, 'pushState');
jest.spyOn(Api, 'projectUsers').mockResolvedValue(users);
jest.spyOn(Api, 'branches').mockResolvedValue({ data: branches });
});
afterEach(() => {
wrapper.destroy();
- mock.restore();
+ mock.reset();
+ window.history.pushState.mockReset();
});
- describe('With permission', () => {
- describe('With pipelines in main tab', () => {
- beforeEach(() => {
- mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
- createComponent();
- return waitForPromises();
- });
+ describe('when pipelines are not yet loaded', () => {
+ beforeEach(async () => {
+ createComponent();
+ await nextTick();
+ });
- it('renders tabs', () => {
- expect(findTab('all').text()).toContain('All');
- });
+ it('shows loading state when the app is loading', () => {
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
- it('renders Run Pipeline link', () => {
- expect(findRunPipelineButton().attributes('href')).toBe(paths.newPipelinePath);
+ it('does not display tabs when the first request has not yet been made', () => {
+ expect(findNavigationTabs().exists()).toBe(false);
+ });
+
+ it('does not display buttons', () => {
+ expect(findNavigationControls().exists()).toBe(false);
+ });
+ });
+
+ describe('when there are pipelines in the project', () => {
+ beforeEach(() => {
+ mock
+ .onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } })
+ .reply(200, mockPipelinesResponse);
+ });
+
+ describe('when user has no permissions', () => {
+ beforeEach(async () => {
+ createComponent({ hasGitlabCi: true, canCreatePipeline: false, ...noPermissions });
+ await waitForPromises();
});
- it('renders CI Lint link', () => {
- expect(findCiLintButton().attributes('href')).toBe(paths.ciLintPath);
+ it('renders "All" tab with count different from "0"', () => {
+ expect(findTab('all').text()).toMatchInterpolatedText('All 3');
});
- it('renders Clear Runner Cache button', () => {
- expect(findCleanCacheButton().text()).toBe('Clear Runner Caches');
+ it('does not render buttons', () => {
+ expect(findNavigationControls().exists()).toBe(false);
+
+ expect(findRunPipelineButton().exists()).toBe(false);
+ expect(findCiLintButton().exists()).toBe(false);
+ expect(findCleanCacheButton().exists()).toBe(false);
});
- it('renders pipelines table', () => {
- expect(wrapper.findAll('.gl-responsive-table-row')).toHaveLength(
- pipelines.pipelines.length + 1,
- );
+ it('renders pipelines in a table', () => {
+ expect(findPipelinesTable().exists()).toBe(true);
+
+ expect(findPipelineUrlLinks()).toHaveLength(mockPipelinesIds.length);
+ expect(findPipelineUrlLinks().at(0).text()).toBe(`#${mockPipelinesIds[0]}`);
+ expect(findPipelineUrlLinks().at(1).text()).toBe(`#${mockPipelinesIds[1]}`);
+ expect(findPipelineUrlLinks().at(2).text()).toBe(`#${mockPipelinesIds[2]}`);
});
});
- describe('Without pipelines on main tab with CI', () => {
- beforeEach(() => {
- mock.onGet('twitter/flight/pipelines.json').reply(200, {
- pipelines: [],
- count: {
- all: 0,
- pending: 0,
- running: 0,
- finished: 0,
- },
- });
-
+ describe('when user has permissions', () => {
+ beforeEach(async () => {
createComponent();
+ await waitForPromises();
+ });
- return waitForPromises();
+ it('should set up navigation tabs', () => {
+ expect(findNavigationTabs().props('tabs')).toEqual([
+ { name: 'All', scope: 'all', count: '3', isActive: true },
+ { name: 'Finished', scope: 'finished', count: undefined, isActive: false },
+ { name: 'Branches', scope: 'branches', isActive: false },
+ { name: 'Tags', scope: 'tags', isActive: false },
+ ]);
});
- it('renders tabs', () => {
- expect(findTab('all').text()).toContain('All');
+ it('renders "All" tab with count different from "0"', () => {
+ expect(findTab('all').text()).toMatchInterpolatedText('All 3');
+ });
+
+ it('should render other navigation tabs', () => {
+ expect(findTab('finished').text()).toBe('Finished');
+ expect(findTab('branches').text()).toBe('Branches');
+ expect(findTab('tags').text()).toBe('Tags');
+ });
+
+ it('shows navigation controls', () => {
+ expect(findNavigationControls().exists()).toBe(true);
});
it('renders Run Pipeline link', () => {
@@ -166,549 +206,513 @@ describe('Pipelines', () => {
expect(findCleanCacheButton().text()).toBe('Clear Runner Caches');
});
- it('renders tab empty state', () => {
- expect(findBlankState().text()).toBe('There are currently no pipelines.');
- });
-
- it('renders tab empty state finished scope', () => {
- wrapper.vm.scope = 'finished';
+ it('renders pipelines in a table', () => {
+ expect(findPipelinesTable().exists()).toBe(true);
- return nextTick().then(() => {
- expect(findBlankState().text()).toBe('There are currently no finished pipelines.');
- });
+ expect(findPipelineUrlLinks()).toHaveLength(mockPipelinesIds.length);
+ expect(findPipelineUrlLinks().at(0).text()).toBe(`#${mockPipelinesIds[0]}`);
+ expect(findPipelineUrlLinks().at(1).text()).toBe(`#${mockPipelinesIds[1]}`);
+ expect(findPipelineUrlLinks().at(2).text()).toBe(`#${mockPipelinesIds[2]}`);
});
- });
-
- describe('Without pipelines nor CI', () => {
- beforeEach(() => {
- mock.onGet('twitter/flight/pipelines.json').reply(200, {
- pipelines: [],
- count: {
- all: 0,
- pending: 0,
- running: 0,
- finished: 0,
- },
- });
- createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths });
+ describe('when user goes to a tab', () => {
+ const goToTab = (tab) => {
+ findNavigationTabs().vm.$emit('onChangeTab', tab);
+ };
- return waitForPromises();
- });
+ describe('when the scope in the tab has pipelines', () => {
+ const mockFinishedPipeline = mockPipelinesResponse.pipelines[0];
- it('renders empty state', () => {
- expect(findEmptyState().find('h4').text()).toBe('Build with confidence');
- expect(findEmptyState().find(GlButton).attributes('href')).toBe(paths.helpPagePath);
- });
+ beforeEach(async () => {
+ mock
+ .onGet(mockPipelinesEndpoint, { params: { scope: 'finished', page: '1' } })
+ .reply(200, {
+ pipelines: [mockFinishedPipeline],
+ count: mockPipelinesResponse.count,
+ });
- it('does not render tabs nor buttons', () => {
- expect(findTab('all').exists()).toBe(false);
- expect(findRunPipelineButton().exists()).toBeFalsy();
- expect(findCiLintButton().exists()).toBeFalsy();
- expect(findCleanCacheButton().exists()).toBeFalsy();
- });
- });
+ goToTab('finished');
- describe('When API returns error', () => {
- beforeEach(() => {
- mock.onGet('twitter/flight/pipelines.json').reply(500, {});
- createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths });
+ await waitForPromises();
+ });
- return waitForPromises();
- });
+ it('should filter pipelines', async () => {
+ expect(findPipelinesTable().exists()).toBe(true);
- it('renders tabs', () => {
- expect(findTab('all').text()).toContain('All');
- });
+ expect(findPipelineUrlLinks()).toHaveLength(1);
+ expect(findPipelineUrlLinks().at(0).text()).toBe(`#${mockFinishedPipeline.id}`);
+ });
- it('renders buttons', () => {
- expect(findRunPipelineButton().attributes('href')).toBe(paths.newPipelinePath);
+ it('should update browser bar', () => {
+ expect(window.history.pushState).toHaveBeenCalledTimes(1);
+ expect(window.history.pushState).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.anything(),
+ `${window.location.pathname}?scope=finished&page=1`,
+ );
+ });
+ });
- expect(findCiLintButton().attributes('href')).toBe(paths.ciLintPath);
- expect(findCleanCacheButton().text()).toBe('Clear Runner Caches');
- });
+ describe('when the scope in the tab is empty', () => {
+ beforeEach(async () => {
+ mock
+ .onGet(mockPipelinesEndpoint, { params: { scope: 'branches', page: '1' } })
+ .reply(200, {
+ pipelines: [],
+ count: mockPipelinesResponse.count,
+ });
- it('renders error state', () => {
- expect(findBlankState().text()).toContain('There was an error fetching the pipelines.');
- });
- });
- });
+ goToTab('branches');
- describe('Without permission', () => {
- describe('With pipelines in main tab', () => {
- beforeEach(() => {
- mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
+ await waitForPromises();
+ });
- createComponent({ hasGitlabCi: false, canCreatePipeline: false, ...noPermissions });
+ it('should filter pipelines', async () => {
+ expect(findBlankState().text()).toBe('There are currently no pipelines.');
+ });
- return waitForPromises();
+ it('should update browser bar', () => {
+ expect(window.history.pushState).toHaveBeenCalledTimes(1);
+ expect(window.history.pushState).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.anything(),
+ `${window.location.pathname}?scope=branches&page=1`,
+ );
+ });
+ });
});
- it('renders tabs', () => {
- expect(findTab('all').text()).toContain('All');
- });
+ describe('when user triggers a filtered search', () => {
+ const mockFilteredPipeline = mockPipelinesResponse.pipelines[1];
- it('does not render buttons', () => {
- expect(findRunPipelineButton().exists()).toBeFalsy();
- expect(findCiLintButton().exists()).toBeFalsy();
- expect(findCleanCacheButton().exists()).toBeFalsy();
- });
+ let expectedParams;
- it('renders pipelines table', () => {
- expect(wrapper.findAll('.gl-responsive-table-row')).toHaveLength(
- pipelines.pipelines.length + 1,
- );
- });
- });
+ beforeEach(async () => {
+ expectedParams = {
+ page: '1',
+ scope: 'all',
+ username: 'root',
+ ref: 'master',
+ status: 'pending',
+ };
+
+ mock
+ .onGet(mockPipelinesEndpoint, {
+ params: expectedParams,
+ })
+ .replyOnce(200, {
+ pipelines: [mockFilteredPipeline],
+ count: mockPipelinesResponse.count,
+ });
- describe('Without pipelines on main tab with CI', () => {
- beforeEach(() => {
- mock.onGet('twitter/flight/pipelines.json').reply(200, {
- pipelines: [],
- count: {
- all: 0,
- pending: 0,
- running: 0,
- finished: 0,
- },
+ findFilteredSearch().vm.$emit('submit', mockSearch);
+
+ await waitForPromises();
});
- createComponent({ hasGitlabCi: true, canCreatePipeline: false, ...noPermissions });
+ it('requests data with query params on filter submit', async () => {
+ expect(mock.history.get[1].params).toEqual(expectedParams);
+ });
- return waitForPromises();
- });
+ it('renders filtered pipelines', async () => {
+ expect(findPipelineUrlLinks()).toHaveLength(1);
+ expect(findPipelineUrlLinks().at(0).text()).toBe(`#${mockFilteredPipeline.id}`);
+ });
- it('renders tabs', () => {
- expect(findTab('all').text()).toContain('All');
+ it('should update browser bar', () => {
+ expect(window.history.pushState).toHaveBeenCalledTimes(1);
+ expect(window.history.pushState).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.anything(),
+ `${window.location.pathname}?page=1&scope=all&username=root&ref=master&status=pending`,
+ );
+ });
});
- it('does not render buttons', () => {
- expect(findRunPipelineButton().exists()).toBeFalsy();
- expect(findCiLintButton().exists()).toBeFalsy();
- expect(findCleanCacheButton().exists()).toBeFalsy();
- });
+ describe('when user triggers a filtered search with raw text', () => {
+ beforeEach(async () => {
+ findFilteredSearch().vm.$emit('submit', ['rawText']);
- it('renders tab empty state', () => {
- expect(wrapper.find('.empty-state h4').text()).toBe('There are currently no pipelines.');
- });
- });
+ await waitForPromises();
+ });
- describe('Without pipelines nor CI', () => {
- beforeEach(() => {
- mock.onGet('twitter/flight/pipelines.json').reply(200, {
- pipelines: [],
- count: {
- all: 0,
- pending: 0,
- running: 0,
- finished: 0,
- },
+ it('requests data with query params on filter submit', async () => {
+ expect(mock.history.get[1].params).toEqual({ page: '1', scope: 'all' });
});
- createComponent({ hasGitlabCi: false, canCreatePipeline: false, ...noPermissions });
+ it('displays a warning message if raw text search is used', () => {
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledWith(RAW_TEXT_WARNING, 'warning');
+ });
- return waitForPromises();
+ it('should update browser bar', () => {
+ expect(window.history.pushState).toHaveBeenCalledTimes(1);
+ expect(window.history.pushState).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.anything(),
+ `${window.location.pathname}?page=1&scope=all`,
+ );
+ });
});
+ });
+ });
- it('renders empty state without button to set CI', () => {
- expect(findEmptyState().text()).toBe(
- 'This project is not currently set up to run pipelines.',
- );
+ describe('when there are multiple pages of pipelines', () => {
+ const mockPageSize = 2;
+ const mockPageHeaders = ({ page = 1 } = {}) => {
+ return {
+ 'X-PER-PAGE': `${mockPageSize}`,
+ 'X-PREV-PAGE': `${page - 1}`,
+ 'X-PAGE': `${page}`,
+ 'X-NEXT-PAGE': `${page + 1}`,
+ };
+ };
+ const [firstPage, secondPage] = chunk(mockPipelinesResponse.pipelines, mockPageSize);
+
+ const goToPage = (page) => {
+ findTablePagination().find(GlPagination).vm.$emit('input', page);
+ };
+
+ beforeEach(async () => {
+ mock.onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } }).reply(
+ 200,
+ {
+ pipelines: firstPage,
+ count: mockPipelinesResponse.count,
+ },
+ mockPageHeaders({ page: 1 }),
+ );
+ mock.onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '2' } }).reply(
+ 200,
+ {
+ pipelines: secondPage,
+ count: mockPipelinesResponse.count,
+ },
+ mockPageHeaders({ page: 2 }),
+ );
- expect(findEmptyState().find(GlButton).exists()).toBeFalsy();
- });
+ createComponent();
- it('does not render tabs or buttons', () => {
- expect(findTab('all').exists()).toBe(false);
- expect(findRunPipelineButton().exists()).toBeFalsy();
- expect(findCiLintButton().exists()).toBeFalsy();
- expect(findCleanCacheButton().exists()).toBeFalsy();
- });
+ await waitForPromises();
});
- describe('When API returns error', () => {
- beforeEach(() => {
- mock.onGet('twitter/flight/pipelines.json').reply(500, {});
-
- createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...noPermissions });
+ it('shows the first page of pipelines', () => {
+ expect(findPipelineUrlLinks()).toHaveLength(firstPage.length);
+ expect(findPipelineUrlLinks().at(0).text()).toBe(`#${firstPage[0].id}`);
+ expect(findPipelineUrlLinks().at(1).text()).toBe(`#${firstPage[1].id}`);
+ });
- return waitForPromises();
- });
+ it('should not update browser bar', () => {
+ expect(window.history.pushState).not.toHaveBeenCalled();
+ });
- it('renders tabs', () => {
- expect(findTab('all').text()).toContain('All');
+ describe('when user goes to next page', () => {
+ beforeEach(async () => {
+ goToPage(2);
+ await waitForPromises();
});
- it('does not renders buttons', () => {
- expect(findRunPipelineButton().exists()).toBeFalsy();
- expect(findCiLintButton().exists()).toBeFalsy();
- expect(findCleanCacheButton().exists()).toBeFalsy();
+ it('should update page and keep scope the same scope', () => {
+ expect(findPipelineUrlLinks()).toHaveLength(secondPage.length);
+ expect(findPipelineUrlLinks().at(0).text()).toBe(`#${secondPage[0].id}`);
});
- it('renders error state', () => {
- expect(wrapper.find('.empty-state').text()).toContain(
- 'There was an error fetching the pipelines.',
+ it('should update browser bar', () => {
+ expect(window.history.pushState).toHaveBeenCalledTimes(1);
+ expect(window.history.pushState).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.anything(),
+ `${window.location.pathname}?page=2&scope=all`,
);
});
});
});
- describe('successful request', () => {
- describe('with pipelines', () => {
- beforeEach(() => {
- mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
+ describe('when pipelines can be polled', () => {
+ beforeEach(() => {
+ const emptyResponse = {
+ pipelines: [],
+ count: { all: '0' },
+ };
+ // Mock no pipelines in the first attempt
+ mock
+ .onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } })
+ .replyOnce(200, emptyResponse, {
+ 'POLL-INTERVAL': 100,
+ });
+ // Mock pipelines in the next attempt
+ mock
+ .onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } })
+ .reply(200, mockPipelinesResponse, {
+ 'POLL-INTERVAL': 100,
+ });
+ });
+
+ describe('data is loaded for the first time', () => {
+ beforeEach(async () => {
createComponent();
- return waitForPromises();
+ await waitForPromises();
});
- it('should render table', () => {
- expect(wrapper.findAll('.gl-responsive-table-row')).toHaveLength(
- pipelines.pipelines.length + 1,
- );
+ it('shows tabs', () => {
+ expect(findNavigationTabs().exists()).toBe(true);
});
- it('should set up navigation tabs', () => {
- expect(findNavigationTabs().props('tabs')).toEqual([
- { name: 'All', scope: 'all', count: '3', isActive: true },
- { name: 'Finished', scope: 'finished', count: undefined, isActive: false },
- { name: 'Branches', scope: 'branches', isActive: false },
- { name: 'Tags', scope: 'tags', isActive: false },
- ]);
+ it('should update page and keep scope the same scope', () => {
+ expect(findPipelineUrlLinks()).toHaveLength(0);
});
- it('should render navigation tabs', () => {
- expect(findTab('all').html()).toContain('All');
- expect(findTab('finished').text()).toContain('Finished');
- expect(findTab('branches').text()).toContain('Branches');
- expect(findTab('tags').text()).toContain('Tags');
- });
-
- it('should make an API request when using tabs', () => {
- createComponent({ hasGitlabCi: true, canCreatePipeline: true, ...paths });
- jest.spyOn(wrapper.vm.service, 'getPipelines');
-
- return waitForPromises().then(() => {
- findTab('finished').trigger('click');
-
- expect(wrapper.vm.service.getPipelines).toHaveBeenCalledWith({
- scope: 'finished',
- page: '1',
- });
+ describe('data is loaded for a second time', () => {
+ beforeEach(async () => {
+ jest.runOnlyPendingTimers();
+ await waitForPromises();
});
- });
-
- describe('with pagination', () => {
- it('should make an API request when using pagination', () => {
- createComponent({ hasGitlabCi: true, canCreatePipeline: true, ...paths });
- jest.spyOn(wrapper.vm.service, 'getPipelines');
- return waitForPromises()
- .then(() => {
- // Mock pagination
- wrapper.vm.store.state.pageInfo = {
- page: 1,
- total: 10,
- perPage: 2,
- nextPage: 2,
- totalPages: 5,
- };
+ it('shows tabs', () => {
+ expect(findNavigationTabs().exists()).toBe(true);
+ });
- return nextTick();
- })
- .then(() => {
- wrapper.find('.next-page-item').trigger('click');
- expect(wrapper.vm.service.getPipelines).toHaveBeenCalledWith({
- scope: 'all',
- page: '2',
- });
- });
+ it('is loading after a time', async () => {
+ expect(findPipelineUrlLinks()).toHaveLength(mockPipelinesIds.length);
+ expect(findPipelineUrlLinks().at(0).text()).toBe(`#${mockPipelinesIds[0]}`);
+ expect(findPipelineUrlLinks().at(1).text()).toBe(`#${mockPipelinesIds[1]}`);
+ expect(findPipelineUrlLinks().at(2).text()).toBe(`#${mockPipelinesIds[2]}`);
});
});
});
});
- describe('User Interaction', () => {
- let updateContentMock;
-
+ describe('when no pipelines exist', () => {
beforeEach(() => {
- jest.spyOn(window.history, 'pushState').mockImplementation(() => null);
- });
-
- beforeEach(() => {
- mock.onGet(paths.endpoint).reply(200, pipelines);
- createComponent();
-
- updateContentMock = jest.spyOn(wrapper.vm, 'updateContent');
-
- return waitForPromises();
+ mock.onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } }).reply(200, {
+ pipelines: [],
+ count: { all: '0' },
+ });
});
- describe('when user changes tabs', () => {
- it('should set page to 1', () => {
- findNavigationTabs().vm.$emit('onChangeTab', 'running');
+ describe('when CI is enabled and user has permissions', () => {
+ beforeEach(async () => {
+ createComponent();
+ await waitForPromises();
+ });
- expect(updateContentMock).toHaveBeenCalledWith({ scope: 'running', page: '1' });
+ it('renders tab with count of "0"', () => {
+ expect(findNavigationTabs().exists()).toBe(true);
+ expect(findTab('all').text()).toMatchInterpolatedText('All 0');
});
- });
- describe('when user changes page', () => {
- it('should update page and keep scope', () => {
- findTablePagination().vm.change(4);
+ it('renders Run Pipeline link', () => {
+ expect(findRunPipelineButton().attributes('href')).toBe(paths.newPipelinePath);
+ });
- expect(updateContentMock).toHaveBeenCalledWith({ scope: wrapper.vm.scope, page: '4' });
+ it('renders CI Lint link', () => {
+ expect(findCiLintButton().attributes('href')).toBe(paths.ciLintPath);
});
- });
- describe('updates results when a staged is clicked', () => {
- beforeEach(() => {
- const copyPipeline = { ...pipelineWithStages };
- copyPipeline.id += 1;
- mock
- .onGet('twitter/flight/pipelines.json')
- .reply(
- 200,
- {
- pipelines: [pipelineWithStages],
- count: {
- all: 1,
- finished: 1,
- pending: 0,
- running: 0,
- },
- },
- {
- 'POLL-INTERVAL': 100,
- },
- )
- .onGet(pipelineWithStages.details.stages[0].dropdown_path)
- .reply(200, stageReply);
+ it('renders Clear Runner Cache button', () => {
+ expect(findCleanCacheButton().text()).toBe('Clear Runner Caches');
+ });
- createComponent();
+ it('renders empty state', () => {
+ expect(findBlankState().text()).toBe('There are currently no pipelines.');
});
- describe('when a request is being made', () => {
- it('stops polling, cancels the request, & restarts polling', () => {
- const stopMock = jest.spyOn(wrapper.vm.poll, 'stop');
- const restartMock = jest.spyOn(wrapper.vm.poll, 'restart');
- const cancelMock = jest.spyOn(wrapper.vm.service.cancelationSource, 'cancel');
- mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
-
- return waitForPromises()
- .then(() => {
- wrapper.vm.isMakingRequest = true;
- findStagesDropdown().trigger('click');
- })
- .then(() => {
- expect(cancelMock).toHaveBeenCalled();
- expect(stopMock).toHaveBeenCalled();
- expect(restartMock).toHaveBeenCalled();
- });
+ it('renders tab empty state finished scope', async () => {
+ mock.onGet(mockPipelinesEndpoint, { params: { scope: 'finished', page: '1' } }).reply(200, {
+ pipelines: [],
+ count: { all: '0' },
});
- });
- describe('when no request is being made', () => {
- it('stops polling & restarts polling', () => {
- const stopMock = jest.spyOn(wrapper.vm.poll, 'stop');
- const restartMock = jest.spyOn(wrapper.vm.poll, 'restart');
- mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
+ findNavigationTabs().vm.$emit('onChangeTab', 'finished');
- return waitForPromises()
- .then(() => {
- findStagesDropdown().trigger('click');
- expect(stopMock).toHaveBeenCalled();
- })
- .then(() => {
- expect(restartMock).toHaveBeenCalled();
- });
- });
- });
- });
- });
+ await waitForPromises();
- describe('Rendered content', () => {
- beforeEach(() => {
- createComponent();
+ expect(findBlankState().text()).toBe('There are currently no finished pipelines.');
+ });
});
- describe('displays different content', () => {
- it('shows loading state when the app is loading', () => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ describe('when CI is not enabled and user has permissions', () => {
+ beforeEach(async () => {
+ createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths });
+ await waitForPromises();
});
- it('shows error state when app has error', () => {
- wrapper.vm.hasError = true;
- wrapper.vm.isLoading = false;
-
- return nextTick().then(() => {
- expect(findBlankState().props('message')).toBe(
- 'There was an error fetching the pipelines. Try again in a few moments or contact your support team.',
- );
- });
+ it('renders empty state', () => {
+ expect(findEmptyState().find('[data-testid="header-text"]').text()).toBe(
+ 'Build with confidence',
+ );
+ expect(findEmptyState().find('[data-testid="info-text"]').text()).toContain(
+ 'GitLab CI/CD can automatically build, test, and deploy your code.',
+ );
+ expect(findEmptyState().find(GlButton).text()).toBe('Get started with CI/CD');
+ expect(findEmptyState().find(GlButton).attributes('href')).toBe(paths.helpPagePath);
});
- it('shows table list when app has pipelines', () => {
- wrapper.vm.isLoading = false;
- wrapper.vm.hasError = false;
- wrapper.vm.state.pipelines = pipelines.pipelines;
-
- return nextTick().then(() => {
- expect(wrapper.find(PipelinesTableComponent).exists()).toBe(true);
- });
+ it('does not render tabs nor buttons', () => {
+ expect(findNavigationTabs().exists()).toBe(false);
+ expect(findTab('all').exists()).toBe(false);
+ expect(findRunPipelineButton().exists()).toBe(false);
+ expect(findCiLintButton().exists()).toBe(false);
+ expect(findCleanCacheButton().exists()).toBe(false);
});
+ });
- it('shows empty tab when app does not have pipelines but project has pipelines', () => {
- wrapper.vm.state.count.all = 10;
- wrapper.vm.isLoading = false;
-
- return nextTick().then(() => {
- expect(findBlankState().exists()).toBe(true);
- expect(findBlankState().props('message')).toBe('There are currently no pipelines.');
- });
+ describe('when CI is not enabled and user has no permissions', () => {
+ beforeEach(async () => {
+ createComponent({ hasGitlabCi: false, canCreatePipeline: false, ...noPermissions });
+ await waitForPromises();
});
- it('shows empty tab when project has CI', () => {
- wrapper.vm.isLoading = false;
+ it('renders empty state without button to set CI', () => {
+ expect(findEmptyState().text()).toBe(
+ 'This project is not currently set up to run pipelines.',
+ );
- return nextTick().then(() => {
- expect(findBlankState().exists()).toBe(true);
- expect(findBlankState().props('message')).toBe('There are currently no pipelines.');
- });
+ expect(findEmptyState().find(GlButton).exists()).toBe(false);
});
- it('shows empty state when project does not have pipelines nor CI', () => {
- createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths });
-
- wrapper.vm.isLoading = false;
-
- return nextTick().then(() => {
- expect(wrapper.find(EmptyState).exists()).toBe(true);
- });
+ it('does not render tabs or buttons', () => {
+ expect(findTab('all').exists()).toBe(false);
+ expect(findRunPipelineButton().exists()).toBe(false);
+ expect(findCiLintButton().exists()).toBe(false);
+ expect(findCleanCacheButton().exists()).toBe(false);
});
});
- describe('displays tabs', () => {
- it('returns true when state is loading & has already made the first request', () => {
- wrapper.vm.isLoading = true;
- wrapper.vm.hasMadeRequest = true;
+ describe('when CI is enabled and user has no permissions', () => {
+ beforeEach(() => {
+ createComponent({ hasGitlabCi: true, canCreatePipeline: false, ...noPermissions });
- return nextTick().then(() => {
- expect(findNavigationTabs().exists()).toBe(true);
- });
+ return waitForPromises();
});
- it('returns true when state is tableList & has already made the first request', () => {
- wrapper.vm.isLoading = false;
- wrapper.vm.state.pipelines = pipelines.pipelines;
- wrapper.vm.hasMadeRequest = true;
-
- return nextTick().then(() => {
- expect(findNavigationTabs().exists()).toBe(true);
- });
+ it('renders tab with count of "0"', () => {
+ expect(findTab('all').text()).toMatchInterpolatedText('All 0');
});
- it('returns true when state is error & has already made the first request', () => {
- wrapper.vm.isLoading = false;
- wrapper.vm.hasError = true;
- wrapper.vm.hasMadeRequest = true;
+ it('does not render buttons', () => {
+ expect(findRunPipelineButton().exists()).toBe(false);
+ expect(findCiLintButton().exists()).toBe(false);
+ expect(findCleanCacheButton().exists()).toBe(false);
+ });
- return nextTick().then(() => {
- expect(findNavigationTabs().exists()).toBe(true);
- });
+ it('renders empty state', () => {
+ expect(findBlankState().text()).toBe('There are currently no pipelines.');
});
+ });
+ });
- it('returns true when state is empty tab & has already made the first request', () => {
- wrapper.vm.isLoading = false;
- wrapper.vm.state.count.all = 10;
- wrapper.vm.hasMadeRequest = true;
+ describe('when a pipeline with stages exists', () => {
+ describe('updates results when a staged is clicked', () => {
+ let stopMock;
+ let restartMock;
+ let cancelMock;
- return nextTick().then(() => {
- expect(findNavigationTabs().exists()).toBe(true);
- });
- });
+ beforeEach(() => {
+ mock.onGet(mockPipelinesEndpoint, { scope: 'all', page: '1' }).reply(
+ 200,
+ {
+ pipelines: [pipelineWithStages],
+ count: { all: '1' },
+ },
+ {
+ 'POLL-INTERVAL': 100,
+ },
+ );
+ mock.onGet(pipelineWithStages.details.stages[0].dropdown_path).reply(200, stageReply);
- it('returns false when has not made first request', () => {
- wrapper.vm.hasMadeRequest = false;
+ createComponent();
- return nextTick().then(() => {
- expect(findNavigationTabs().exists()).toBe(false);
- });
+ stopMock = jest.spyOn(wrapper.vm.poll, 'stop');
+ restartMock = jest.spyOn(wrapper.vm.poll, 'restart');
+ cancelMock = jest.spyOn(wrapper.vm.service.cancelationSource, 'cancel');
});
- it('returns false when state is empty state', () => {
- createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths });
-
- wrapper.vm.isLoading = false;
- wrapper.vm.hasMadeRequest = true;
+ describe('when a request is being made', () => {
+ beforeEach(async () => {
+ mock.onGet(mockPipelinesEndpoint).reply(200, mockPipelinesResponse);
- return nextTick().then(() => {
- expect(findNavigationTabs().exists()).toBe(false);
+ await waitForPromises();
});
- });
- });
- describe('displays buttons', () => {
- it('returns true when it has paths & has made the first request', () => {
- wrapper.vm.hasMadeRequest = true;
+ it('stops polling, cancels the request, & restarts polling', async () => {
+ // Mock init a polling cycle
+ wrapper.vm.poll.options.notificationCallback(true);
+
+ findStagesDropdown().trigger('click');
- return nextTick().then(() => {
- expect(findNavigationControls().exists()).toBe(true);
+ await waitForPromises();
+
+ expect(cancelMock).toHaveBeenCalled();
+ expect(stopMock).toHaveBeenCalled();
+ expect(restartMock).toHaveBeenCalled();
});
- });
- it('returns false when it has not made the first request', () => {
- wrapper.vm.hasMadeRequest = false;
+ it('stops polling & restarts polling', async () => {
+ findStagesDropdown().trigger('click');
- return nextTick().then(() => {
- expect(findNavigationControls().exists()).toBe(false);
+ expect(cancelMock).not.toHaveBeenCalled();
+ expect(stopMock).toHaveBeenCalled();
+ expect(restartMock).toHaveBeenCalled();
});
});
});
});
- describe('Pipeline filters', () => {
- let updateContentMock;
-
- beforeEach(() => {
- mock.onGet(paths.endpoint).reply(200, pipelines);
- createComponent();
+ describe('when pipelines cannot be loaded', () => {
+ beforeEach(async () => {
+ mock.onGet(mockPipelinesEndpoint).reply(500, {});
+ });
- updateContentMock = jest.spyOn(wrapper.vm, 'updateContent');
+ describe('when user has no permissions', () => {
+ beforeEach(async () => {
+ createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...noPermissions });
- return waitForPromises();
- });
+ await waitForPromises();
+ });
- it('updates request data and query params on filter submit', async () => {
- const expectedQueryParams = {
- page: '1',
- scope: 'all',
- username: 'root',
- ref: 'master',
- status: 'pending',
- };
+ it('renders tabs', () => {
+ expect(findNavigationTabs().exists()).toBe(true);
+ expect(findTab('all').text()).toBe('All');
+ });
- findFilteredSearch().vm.$emit('submit', mockSearch);
- await nextTick();
+ it('does not render buttons', () => {
+ expect(findRunPipelineButton().exists()).toBe(false);
+ expect(findCiLintButton().exists()).toBe(false);
+ expect(findCleanCacheButton().exists()).toBe(false);
+ });
- expect(wrapper.vm.requestData).toEqual(expectedQueryParams);
- expect(updateContentMock).toHaveBeenCalledWith(expectedQueryParams);
+ it('shows error state', () => {
+ expect(findBlankState().text()).toBe(
+ 'There was an error fetching the pipelines. Try again in a few moments or contact your support team.',
+ );
+ });
});
- it('does not add query params if raw text search is used', async () => {
- const expectedQueryParams = { page: '1', scope: 'all' };
+ describe('when user has permissions', () => {
+ beforeEach(async () => {
+ createComponent();
- findFilteredSearch().vm.$emit('submit', ['rawText']);
- await nextTick();
+ await waitForPromises();
+ });
- expect(wrapper.vm.requestData).toEqual(expectedQueryParams);
- expect(updateContentMock).toHaveBeenCalledWith(expectedQueryParams);
- });
+ it('renders tabs', () => {
+ expect(findTab('all').text()).toBe('All');
+ });
+
+ it('renders buttons', () => {
+ expect(findRunPipelineButton().attributes('href')).toBe(paths.newPipelinePath);
- it('displays a warning message if raw text search is used', () => {
- findFilteredSearch().vm.$emit('submit', ['rawText']);
+ expect(findCiLintButton().attributes('href')).toBe(paths.ciLintPath);
+ expect(findCleanCacheButton().text()).toBe('Clear Runner Caches');
+ });
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith(RAW_TEXT_WARNING, 'warning');
+ it('shows error state', () => {
+ expect(findBlankState().text()).toBe(
+ 'There was an error fetching the pipelines. Try again in a few moments or contact your support team.',
+ );
+ });
});
});
});
diff --git a/spec/frontend/pipelines/pipelines_table_row_spec.js b/spec/frontend/pipelines/pipelines_table_row_spec.js
index 9cdd24b2ab5..660651547fc 100644
--- a/spec/frontend/pipelines/pipelines_table_row_spec.js
+++ b/spec/frontend/pipelines/pipelines_table_row_spec.js
@@ -155,7 +155,9 @@ describe('Pipelines Table Row', () => {
it('should render an icon for each stage', () => {
expect(
- wrapper.findAll('.table-section:nth-child(4) .js-builds-dropdown-button').length,
+ wrapper.findAll(
+ '.table-section:nth-child(4) [data-testid="mini-pipeline-graph-dropdown-toggle"]',
+ ).length,
).toEqual(pipeline.details.stages.length);
});
});
diff --git a/spec/frontend/pipelines/stage_spec.js b/spec/frontend/pipelines/stage_spec.js
index e4782a1dab1..87b43558252 100644
--- a/spec/frontend/pipelines/stage_spec.js
+++ b/spec/frontend/pipelines/stage_spec.js
@@ -1,6 +1,8 @@
import 'bootstrap/js/dist/dropdown';
+import { GlDropdown } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
import StageComponent from '~/pipelines/components/pipelines_list/stage.vue';
import eventHub from '~/pipelines/event_hub';
@@ -9,6 +11,7 @@ import { stageReply } from './mock_data';
describe('Pipelines stage component', () => {
let wrapper;
let mock;
+ let glFeatures;
const defaultProps = {
stage: {
@@ -22,8 +25,6 @@ describe('Pipelines stage component', () => {
updateDropdown: false,
};
- const isDropdownOpen = () => wrapper.classes('show');
-
const createComponent = (props = {}) => {
wrapper = mount(StageComponent, {
attachTo: document.body,
@@ -31,110 +32,265 @@ describe('Pipelines stage component', () => {
...defaultProps,
...props,
},
+ provide: {
+ glFeatures,
+ },
});
};
beforeEach(() => {
mock = new MockAdapter(axios);
+ jest.spyOn(eventHub, '$emit');
+ glFeatures = {};
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
+ eventHub.$emit.mockRestore();
mock.restore();
});
- describe('default', () => {
- beforeEach(() => {
- createComponent();
+ describe('when ci_mini_pipeline_gl_dropdown feature flag is disabled', () => {
+ const isDropdownOpen = () => wrapper.classes('show');
+
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should render a dropdown with the status icon', () => {
+ expect(wrapper.attributes('class')).toEqual('dropdown');
+ expect(wrapper.find('svg').exists()).toBe(true);
+ expect(wrapper.find('button').attributes('data-toggle')).toEqual('dropdown');
+ });
});
- it('should render a dropdown with the status icon', () => {
- expect(wrapper.attributes('class')).toEqual('dropdown');
- expect(wrapper.find('svg').exists()).toBe(true);
- expect(wrapper.find('button').attributes('data-toggle')).toEqual('dropdown');
+ describe('with successful request', () => {
+ beforeEach(() => {
+ mock.onGet('path.json').reply(200, stageReply);
+ createComponent();
+ });
+
+ it('should render the received data and emit `clickedDropdown` event', async () => {
+ wrapper.find('button').trigger('click');
+
+ await axios.waitForAll();
+ expect(wrapper.find('.js-builds-dropdown-container ul').text()).toContain(
+ stageReply.latest_statuses[0].name,
+ );
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown');
+ });
});
- });
- describe('with successful request', () => {
- beforeEach(() => {
- mock.onGet('path.json').reply(200, stageReply);
+ it('when request fails should close the dropdown', async () => {
+ mock.onGet('path.json').reply(500);
createComponent();
- });
+ wrapper.find({ ref: 'dropdown' }).trigger('click');
- it('should render the received data and emit `clickedDropdown` event', async () => {
- jest.spyOn(eventHub, '$emit');
- wrapper.find('button').trigger('click');
+ expect(isDropdownOpen()).toBe(true);
+ wrapper.find('button').trigger('click');
await axios.waitForAll();
- expect(wrapper.find('.js-builds-dropdown-container ul').text()).toContain(
- stageReply.latest_statuses[0].name,
- );
- expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown');
+ expect(isDropdownOpen()).toBe(false);
});
- });
- it('when request fails should close the dropdown', async () => {
- mock.onGet('path.json').reply(500);
- createComponent();
- wrapper.find({ ref: 'dropdown' }).trigger('click');
- expect(isDropdownOpen()).toBe(true);
+ describe('update endpoint correctly', () => {
+ beforeEach(() => {
+ const copyStage = { ...stageReply };
+ copyStage.latest_statuses[0].name = 'this is the updated content';
+ mock.onGet('bar.json').reply(200, copyStage);
+ createComponent({
+ stage: {
+ status: {
+ group: 'running',
+ icon: 'status_running',
+ title: 'running',
+ },
+ dropdown_path: 'bar.json',
+ },
+ });
+ return axios.waitForAll();
+ });
+
+ it('should update the stage to request the new endpoint provided', async () => {
+ wrapper.find('button').trigger('click');
+ await axios.waitForAll();
+
+ expect(wrapper.find('.js-builds-dropdown-container ul').text()).toContain(
+ 'this is the updated content',
+ );
+ });
+ });
+
+ describe('pipelineActionRequestComplete', () => {
+ beforeEach(() => {
+ mock.onGet('path.json').reply(200, stageReply);
+ mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200);
+ });
+
+ const clickCiAction = async () => {
+ wrapper.find('button').trigger('click');
+ await axios.waitForAll();
+
+ wrapper.find('.js-ci-action').trigger('click');
+ await axios.waitForAll();
+ };
+
+ describe('within pipeline table', () => {
+ it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', async () => {
+ createComponent({ type: 'PIPELINES_TABLE' });
+
+ await clickCiAction();
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable');
+ });
+ });
+
+ describe('in MR widget', () => {
+ beforeEach(() => {
+ jest.spyOn($.fn, 'dropdown');
+ });
- wrapper.find('button').trigger('click');
- await axios.waitForAll();
+ it('closes the dropdown when `pipelineActionRequestComplete` is triggered', async () => {
+ createComponent();
- expect(isDropdownOpen()).toBe(false);
+ await clickCiAction();
+
+ expect($.fn.dropdown).toHaveBeenCalledWith('toggle');
+ });
+ });
+ });
});
- describe('update endpoint correctly', () => {
+ describe('when ci_mini_pipeline_gl_dropdown feature flag is enabled', () => {
+ const findDropdown = () => wrapper.find(GlDropdown);
+ const findDropdownToggle = () => wrapper.find('button.gl-dropdown-toggle');
+ const findDropdownMenu = () =>
+ wrapper.find('[data-testid="mini-pipeline-graph-dropdown-menu-list"]');
+ const findCiActionBtn = () => wrapper.find('.js-ci-action');
+
+ const openGlDropdown = () => {
+ findDropdownToggle().trigger('click');
+ return new Promise((resolve) => {
+ wrapper.vm.$root.$on('bv::dropdown::show', resolve);
+ });
+ };
+
beforeEach(() => {
- const copyStage = { ...stageReply };
- copyStage.latest_statuses[0].name = 'this is the updated content';
- mock.onGet('bar.json').reply(200, copyStage);
- createComponent({
- stage: {
- status: {
- group: 'running',
- icon: 'status_running',
- title: 'running',
- },
- dropdown_path: 'bar.json',
- },
+ glFeatures = { ciMiniPipelineGlDropdown: true };
+ });
+
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should render a dropdown with the status icon', () => {
+ expect(findDropdown().exists()).toBe(true);
+ expect(findDropdownToggle().classes('gl-dropdown-toggle')).toEqual(true);
+ expect(wrapper.find('[data-testid="status_success_borderless-icon"]').exists()).toBe(true);
});
- return axios.waitForAll();
});
- it('should update the stage to request the new endpoint provided', async () => {
- wrapper.find('button').trigger('click');
+ describe('with successful request', () => {
+ beforeEach(() => {
+ mock.onGet('path.json').reply(200, stageReply);
+ createComponent();
+ });
+
+ it('should render the received data and emit `clickedDropdown` event', async () => {
+ await openGlDropdown();
+ await axios.waitForAll();
+
+ expect(findDropdownMenu().text()).toContain(stageReply.latest_statuses[0].name);
+ expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown');
+ });
+ });
+
+ it('when request fails should close the dropdown', async () => {
+ mock.onGet('path.json').reply(500);
+
+ createComponent();
+
+ await openGlDropdown();
await axios.waitForAll();
- expect(wrapper.find('.js-builds-dropdown-container ul').text()).toContain(
- 'this is the updated content',
- );
+ expect(findDropdown().classes('show')).toBe(false);
});
- });
- describe('pipelineActionRequestComplete', () => {
- beforeEach(() => {
- mock.onGet('path.json').reply(200, stageReply);
- mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200);
+ describe('update endpoint correctly', () => {
+ beforeEach(async () => {
+ const copyStage = { ...stageReply };
+ copyStage.latest_statuses[0].name = 'this is the updated content';
+ mock.onGet('bar.json').reply(200, copyStage);
+ createComponent({
+ stage: {
+ status: {
+ group: 'running',
+ icon: 'status_running',
+ title: 'running',
+ },
+ dropdown_path: 'bar.json',
+ },
+ });
+ await axios.waitForAll();
+ });
- createComponent({ type: 'PIPELINES_TABLE' });
+ it('should update the stage to request the new endpoint provided', async () => {
+ await openGlDropdown();
+ await axios.waitForAll();
+
+ expect(findDropdownMenu().text()).toContain('this is the updated content');
+ });
});
- describe('within pipeline table', () => {
- it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', async () => {
- jest.spyOn(eventHub, '$emit');
+ describe('pipelineActionRequestComplete', () => {
+ beforeEach(() => {
+ mock.onGet('path.json').reply(200, stageReply);
+ mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200);
+ });
- wrapper.find('button').trigger('click');
+ const clickCiAction = async () => {
+ await openGlDropdown();
await axios.waitForAll();
- wrapper.find('.js-ci-action').trigger('click');
+ findCiActionBtn().trigger('click');
await axios.waitForAll();
+ };
+
+ describe('within pipeline table', () => {
+ beforeEach(() => {
+ createComponent({ type: 'PIPELINES_TABLE' });
+ });
+
+ it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', async () => {
+ await clickCiAction();
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable');
+ });
+ });
+
+ describe('in MR widget', () => {
+ beforeEach(() => {
+ jest.spyOn($.fn, 'dropdown');
+ createComponent();
+ });
+
+ it('closes the dropdown when `pipelineActionRequestComplete` is triggered', async () => {
+ const hidden = jest.fn();
+
+ wrapper.vm.$root.$on('bv::dropdown::hide', hidden);
+
+ expect(hidden).toHaveBeenCalledTimes(0);
+
+ await clickCiAction();
- expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable');
+ expect(hidden).toHaveBeenCalledTimes(1);
+ });
});
});
});
diff --git a/spec/frontend/pipelines/test_reports/stores/actions_spec.js b/spec/frontend/pipelines/test_reports/stores/actions_spec.js
index f7ff36c0a46..6258b08dfbb 100644
--- a/spec/frontend/pipelines/test_reports/stores/actions_spec.js
+++ b/spec/frontend/pipelines/test_reports/stores/actions_spec.js
@@ -2,10 +2,10 @@ import MockAdapter from 'axios-mock-adapter';
import { getJSONFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as actions from '~/pipelines/stores/test_reports/actions';
import * as types from '~/pipelines/stores/test_reports/mutation_types';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
jest.mock('~/flash.js');
@@ -16,7 +16,7 @@ describe('Actions TestReports Store', () => {
const testReports = getJSONFixture('pipelines/test_report.json');
const summary = { total_count: 1 };
- const suiteEndpoint = `${TEST_HOST}/tests/:suite_name.json`;
+ const suiteEndpoint = `${TEST_HOST}/tests/suite.json`;
const summaryEndpoint = `${TEST_HOST}/test_reports/summary.json`;
const defaultState = {
suiteEndpoint,
@@ -69,9 +69,8 @@ describe('Actions TestReports Store', () => {
beforeEach(() => {
const buildIds = [1];
testReports.test_suites[0].build_ids = buildIds;
- const endpoint = suiteEndpoint.replace(':suite_name', testReports.test_suites[0].name);
mock
- .onGet(endpoint, { params: { build_ids: buildIds } })
+ .onGet(suiteEndpoint, { params: { build_ids: buildIds } })
.replyOnce(200, testReports.test_suites[0], {});
});
diff --git a/spec/frontend/pipelines/test_reports/stores/getters_spec.js b/spec/frontend/pipelines/test_reports/stores/getters_spec.js
index 7382a6beefa..f8298fdaba5 100644
--- a/spec/frontend/pipelines/test_reports/stores/getters_spec.js
+++ b/spec/frontend/pipelines/test_reports/stores/getters_spec.js
@@ -1,6 +1,10 @@
import { getJSONFixture } from 'helpers/fixtures';
import * as getters from '~/pipelines/stores/test_reports/getters';
-import { iconForTestStatus, formattedTime } from '~/pipelines/stores/test_reports/utils';
+import {
+ iconForTestStatus,
+ formatFilePath,
+ formattedTime,
+} from '~/pipelines/stores/test_reports/utils';
describe('Getters TestReports Store', () => {
let state;
@@ -8,6 +12,7 @@ describe('Getters TestReports Store', () => {
const testReports = getJSONFixture('pipelines/test_report.json');
const defaultState = {
+ blobPath: '/test/blob/path',
testReports,
selectedSuiteIndex: 0,
pageInfo: {
@@ -17,6 +22,7 @@ describe('Getters TestReports Store', () => {
};
const emptyState = {
+ blobPath: '',
testReports: {},
selectedSuite: null,
pageInfo: {
@@ -74,6 +80,7 @@ describe('Getters TestReports Store', () => {
const expected = testReports.test_suites[0].test_cases
.map((x) => ({
...x,
+ filePath: `${state.blobPath}/${formatFilePath(x.file)}`,
formattedTime: formattedTime(x.execution_time),
icon: iconForTestStatus(x.status),
}))
@@ -87,6 +94,70 @@ describe('Getters TestReports Store', () => {
expect(getters.getSuiteTests(state)).toEqual([]);
});
+
+ describe('when a test case classname property is null', () => {
+ it('should return an empty string value for the classname property', () => {
+ const testCases = testReports.test_suites[0].test_cases;
+ setupState({
+ ...defaultState,
+ testReports: {
+ ...testReports,
+ test_suites: [
+ {
+ test_cases: testCases.map((testCase) => ({
+ ...testCase,
+ classname: null,
+ })),
+ },
+ ],
+ },
+ });
+
+ const expected = testCases
+ .map((x) => ({
+ ...x,
+ classname: '',
+ filePath: `${state.blobPath}/${formatFilePath(x.file)}`,
+ formattedTime: formattedTime(x.execution_time),
+ icon: iconForTestStatus(x.status),
+ }))
+ .slice(0, state.pageInfo.perPage);
+
+ expect(getters.getSuiteTests(state)).toEqual(expected);
+ });
+ });
+
+ describe('when a test case name property is null', () => {
+ it('should return an empty string value for the name property', () => {
+ const testCases = testReports.test_suites[0].test_cases;
+ setupState({
+ ...defaultState,
+ testReports: {
+ ...testReports,
+ test_suites: [
+ {
+ test_cases: testCases.map((testCase) => ({
+ ...testCase,
+ name: null,
+ })),
+ },
+ ],
+ },
+ });
+
+ const expected = testCases
+ .map((x) => ({
+ ...x,
+ name: '',
+ filePath: `${state.blobPath}/${formatFilePath(x.file)}`,
+ formattedTime: formattedTime(x.execution_time),
+ icon: iconForTestStatus(x.status),
+ }))
+ .slice(0, state.pageInfo.perPage);
+
+ expect(getters.getSuiteTests(state)).toEqual(expected);
+ });
+ });
});
describe('getSuiteTestCount', () => {
diff --git a/spec/frontend/pipelines/test_reports/stores/utils_spec.js b/spec/frontend/pipelines/test_reports/stores/utils_spec.js
index 7e632d099fc..703fe69026c 100644
--- a/spec/frontend/pipelines/test_reports/stores/utils_spec.js
+++ b/spec/frontend/pipelines/test_reports/stores/utils_spec.js
@@ -1,6 +1,20 @@
-import { formattedTime } from '~/pipelines/stores/test_reports/utils';
+import { formatFilePath, formattedTime } from '~/pipelines/stores/test_reports/utils';
describe('Test reports utils', () => {
+ 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(formatFilePath(file)).toBe(expected);
+ });
+ });
+
describe('formattedTime', () => {
describe('when time is smaller than a second', () => {
it('should return time in milliseconds fixed to 2 decimals', () => {
diff --git a/spec/frontend/pipelines/test_reports/test_case_details_spec.js b/spec/frontend/pipelines/test_reports/test_case_details_spec.js
index bfb8b43778d..e866586a2c3 100644
--- a/spec/frontend/pipelines/test_reports/test_case_details_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_case_details_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
import TestCaseDetails from '~/pipelines/components/test_reports/test_case_details.vue';
import CodeBlock from '~/vue_shared/components/code_block.vue';
@@ -11,12 +11,17 @@ describe('Test case details', () => {
classname: 'spec.test_spec',
name: 'Test#something cool',
formattedTime: '10.04ms',
+ recent_failures: {
+ count: 2,
+ base_branch: 'master',
+ },
system_output: 'Line 42 is broken',
};
const findModal = () => wrapper.find(GlModal);
const findName = () => wrapper.find('[data-testid="test-case-name"]');
const findDuration = () => wrapper.find('[data-testid="test-case-duration"]');
+ const findRecentFailures = () => wrapper.find('[data-testid="test-case-recent-failures"]');
const findSystemOutput = () => wrapper.find('[data-testid="test-case-trace"]');
const createComponent = (testCase = {}) => {
@@ -56,6 +61,36 @@ describe('Test case details', () => {
});
});
+ describe('when test case has recent failures', () => {
+ describe('has only 1 recent failure', () => {
+ it('renders the recent failure', () => {
+ createComponent({ recent_failures: { ...defaultTestCase.recent_failures, count: 1 } });
+
+ expect(findRecentFailures().text()).toContain(
+ `Failed 1 time in ${defaultTestCase.recent_failures.base_branch} in the last 14 days`,
+ );
+ });
+ });
+
+ describe('has more than 1 recent failure', () => {
+ it('renders the recent failures', () => {
+ createComponent();
+
+ expect(findRecentFailures().text()).toContain(
+ `Failed ${defaultTestCase.recent_failures.count} times in ${defaultTestCase.recent_failures.base_branch} in the last 14 days`,
+ );
+ });
+ });
+ });
+
+ describe('when test case does not have recent failures', () => {
+ it('does not render the recent failures', () => {
+ createComponent({ recent_failures: null });
+
+ expect(findRecentFailures().exists()).toBe(false);
+ });
+ });
+
describe('when test case has system output', () => {
it('renders the test case system output', () => {
createComponent();
diff --git a/spec/frontend/pipelines/test_reports/test_reports_spec.js b/spec/frontend/pipelines/test_reports/test_reports_spec.js
index c8ab18b9086..da5763ddf8e 100644
--- a/spec/frontend/pipelines/test_reports/test_reports_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_reports_spec.js
@@ -1,6 +1,6 @@
-import Vuex from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
import { getJSONFixture } from 'helpers/fixtures';
import TestReports from '~/pipelines/components/test_reports/test_reports.vue';
import TestSummary from '~/pipelines/components/test_reports/test_summary.vue';
diff --git a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
index b8fd056610b..a87145cc557 100644
--- a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
@@ -1,10 +1,11 @@
-import Vuex from 'vuex';
+import { GlButton, GlFriendlyWrap, GlLink, GlPagination } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
import { getJSONFixture } from 'helpers/fixtures';
-import { GlButton, GlFriendlyWrap, GlPagination } from '@gitlab/ui';
import SuiteTable from '~/pipelines/components/test_reports/test_suite_table.vue';
-import * as getters from '~/pipelines/stores/test_reports/getters';
import { TestStatus } from '~/pipelines/constants';
+import * as getters from '~/pipelines/stores/test_reports/getters';
+import { formatFilePath } from '~/pipelines/stores/test_reports/utils';
import skippedTestCases from './mock_data';
const localVue = createLocalVue();
@@ -20,15 +21,18 @@ describe('Test reports suite table', () => {
testSuite.test_cases = [...testSuite.test_cases, ...skippedTestCases];
const testCases = testSuite.test_cases;
+ const blobPath = '/test/blob/path';
const noCasesMessage = () => wrapper.find('.js-no-test-cases');
const allCaseRows = () => wrapper.findAll('.js-case-row');
const findCaseRowAtIndex = (index) => wrapper.findAll('.js-case-row').at(index);
+ const findLinkForRow = (row) => row.find(GlLink);
const findIconForRow = (row, status) => row.find(`.ci-status-icon-${status}`);
const createComponent = (suite = testSuite, perPage = 20) => {
store = new Vuex.Store({
state: {
+ blobPath,
testReports: {
test_suites: [suite],
},
@@ -64,7 +68,7 @@ describe('Test reports suite table', () => {
beforeEach(() => createComponent());
it('renders the correct number of rows', () => {
- expect(allCaseRows().length).toBe(testCases.length);
+ expect(allCaseRows()).toHaveLength(testCases.length);
});
it.each([
@@ -82,9 +86,13 @@ describe('Test reports suite table', () => {
it('renders the file name for the test with a copy button', () => {
const { file } = testCases[0];
+ const relativeFile = formatFilePath(file);
+ const filePath = `${blobPath}/${relativeFile}`;
const row = findCaseRowAtIndex(0);
+ const fileLink = findLinkForRow(row);
const button = row.find(GlButton);
+ expect(fileLink.attributes('href')).toBe(filePath);
expect(row.text()).toContain(file);
expect(button.exists()).toBe(true);
expect(button.attributes('data-clipboard-text')).toBe(file);
@@ -106,4 +114,32 @@ describe('Test reports suite table', () => {
expect(wrapper.find(GlPagination).exists()).toBe(true);
});
});
+
+ describe('when a test case classname property is null', () => {
+ it('still renders all test cases', () => {
+ createComponent({
+ ...testSuite,
+ test_cases: testSuite.test_cases.map((testCase) => ({
+ ...testCase,
+ classname: null,
+ })),
+ });
+
+ expect(allCaseRows()).toHaveLength(testCases.length);
+ });
+ });
+
+ describe('when a test case name property is null', () => {
+ it('still renders all test cases', () => {
+ createComponent({
+ ...testSuite,
+ test_cases: testSuite.test_cases.map((testCase) => ({
+ ...testCase,
+ name: null,
+ })),
+ });
+
+ expect(allCaseRows()).toHaveLength(testCases.length);
+ });
+ });
});
diff --git a/spec/frontend/pipelines/test_reports/test_summary_table_spec.js b/spec/frontend/pipelines/test_reports/test_summary_table_spec.js
index b585536ae09..892a3742fea 100644
--- a/spec/frontend/pipelines/test_reports/test_summary_table_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_summary_table_spec.js
@@ -1,5 +1,5 @@
-import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
import { getJSONFixture } from 'helpers/fixtures';
import SummaryTable from '~/pipelines/components/test_reports/test_summary_table.vue';
import * as getters from '~/pipelines/stores/test_reports/getters';
diff --git a/spec/frontend/pipelines/time_ago_spec.js b/spec/frontend/pipelines/time_ago_spec.js
index b7bc8d08a0f..55a19ef5165 100644
--- a/spec/frontend/pipelines/time_ago_spec.js
+++ b/spec/frontend/pipelines/time_ago_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import TimeAgo from '~/pipelines/components/pipelines_list/time_ago.vue';
describe('Timeago component', () => {
diff --git a/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js
index 371ba5a4f9b..7ddbbb3b005 100644
--- a/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js
+++ b/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js
@@ -1,6 +1,6 @@
import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui';
-import { stubComponent } from 'helpers/stub_component';
import { shallowMount } from '@vue/test-utils';
+import { stubComponent } from 'helpers/stub_component';
import Api from '~/api';
import PipelineTriggerAuthorToken from '~/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue';
import { users } from '../mock_data';
diff --git a/spec/frontend/popovers/components/popovers_spec.js b/spec/frontend/popovers/components/popovers_spec.js
index 63e0b3d9c49..0c164d97564 100644
--- a/spec/frontend/popovers/components/popovers_spec.js
+++ b/spec/frontend/popovers/components/popovers_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlPopover } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { useMockMutationObserver } from 'helpers/mock_dom_observer';
import Popovers from '~/popovers/components/popovers.vue';
diff --git a/spec/frontend/profile/account/components/delete_account_modal_spec.js b/spec/frontend/profile/account/components/delete_account_modal_spec.js
index 63e27473979..f1784500baf 100644
--- a/spec/frontend/profile/account/components/delete_account_modal_spec.js
+++ b/spec/frontend/profile/account/components/delete_account_modal_spec.js
@@ -1,8 +1,8 @@
+import { mount } from '@vue/test-utils';
+import { merge } from 'lodash';
import Vue from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
-import { merge } from 'lodash';
-import { mount } from '@vue/test-utils';
import deleteAccountModal from '~/profile/account/components/delete_account_modal.vue';
const GlModalStub = {
diff --git a/spec/frontend/profile/account/components/update_username_spec.js b/spec/frontend/profile/account/components/update_username_spec.js
index 91c3c81ab30..8295d1d43cf 100644
--- a/spec/frontend/profile/account/components/update_username_spec.js
+++ b/spec/frontend/profile/account/components/update_username_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
diff --git a/spec/frontend/profile/preferences/components/__snapshots__/integration_view_spec.js.snap b/spec/frontend/profile/preferences/components/__snapshots__/integration_view_spec.js.snap
deleted file mode 100644
index 2fd1fd6a04e..00000000000
--- a/spec/frontend/profile/preferences/components/__snapshots__/integration_view_spec.js.snap
+++ /dev/null
@@ -1,67 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`IntegrationView component should render IntegrationView properly 1`] = `
-<div
- name="sourcegraph"
->
- <label
- class="label-bold"
- >
-
- Foo
-
- </label>
-
- <gl-link-stub
- class="has-tooltip"
- href="http://foo.com/help"
- title="More information"
- >
- <gl-icon-stub
- class="vertical-align-middle"
- name="question-o"
- size="16"
- />
- </gl-link-stub>
-
- <div
- class="form-group form-check"
- data-testid="profile-preferences-integration-form-group"
- >
- <input
- data-testid="profile-preferences-integration-hidden-field"
- name="user[foo_enabled]"
- type="hidden"
- value="0"
- />
-
- <input
- class="form-check-input"
- data-testid="profile-preferences-integration-checkbox"
- id="user_foo_enabled"
- name="user[foo_enabled]"
- type="checkbox"
- value="1"
- />
-
- <label
- class="form-check-label"
- for="user_foo_enabled"
- >
-
- Enable foo
-
- </label>
-
- <gl-form-text-stub
- tag="div"
- textvariant="muted"
- >
- <integration-help-text-stub
- message="Click %{linkStart}Foo%{linkEnd}!"
- messageurl="http://foo.com"
- />
- </gl-form-text-stub>
- </div>
-</div>
-`;
diff --git a/spec/frontend/profile/preferences/components/__snapshots__/profile_preferences_spec.js.snap b/spec/frontend/profile/preferences/components/__snapshots__/profile_preferences_spec.js.snap
deleted file mode 100644
index 4df92cf86a5..00000000000
--- a/spec/frontend/profile/preferences/components/__snapshots__/profile_preferences_spec.js.snap
+++ /dev/null
@@ -1,51 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`ProfilePreferences component should render ProfilePreferences properly 1`] = `
-<div
- class="row gl-mt-3 js-preferences-form"
->
- <div
- class="col-sm-12"
- >
- <hr
- data-testid="profile-preferences-integrations-rule"
- />
- </div>
-
- <div
- class="col-lg-4 profile-settings-sidebar"
- >
- <h4
- class="gl-mt-0"
- data-testid="profile-preferences-integrations-heading"
- >
-
- Integrations
-
- </h4>
-
- <p>
-
- Customize integrations with third party services.
-
- </p>
- </div>
-
- <div
- class="col-lg-8"
- >
- <integration-view-stub
- config="[object Object]"
- helplink="http://foo.com/help"
- message="Click %{linkStart}Foo%{linkEnd}!"
- messageurl="http://foo.com"
- />
- <integration-view-stub
- config="[object Object]"
- helplink="http://bar.com/help"
- message="Click %{linkStart}Bar%{linkEnd}!"
- messageurl="http://bar.com"
- />
- </div>
-</div>
-`;
diff --git a/spec/frontend/profile/preferences/components/integration_view_spec.js b/spec/frontend/profile/preferences/components/integration_view_spec.js
index 5d55a089119..6ab0c70298c 100644
--- a/spec/frontend/profile/preferences/components/integration_view_spec.js
+++ b/spec/frontend/profile/preferences/components/integration_view_spec.js
@@ -1,9 +1,9 @@
+import { GlFormText } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { GlFormText } from '@gitlab/ui';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import IntegrationView from '~/profile/preferences/components/integration_view.vue';
import IntegrationHelpText from '~/vue_shared/components/integrations_help_text.vue';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { integrationViews, userFields } from '../mock_data';
const viewProps = convertObjectPropsToCamelCase(integrationViews[0]);
@@ -115,10 +115,4 @@ describe('IntegrationView component', () => {
expect(findFormGroupLabel().text()).toBe('Enable foo');
});
-
- it('should render IntegrationView properly', () => {
- wrapper = createComponent();
-
- expect(wrapper.element).toMatchSnapshot();
- });
});
diff --git a/spec/frontend/profile/preferences/components/profile_preferences_spec.js b/spec/frontend/profile/preferences/components/profile_preferences_spec.js
index fcc27d8faaf..82c41178410 100644
--- a/spec/frontend/profile/preferences/components/profile_preferences_spec.js
+++ b/spec/frontend/profile/preferences/components/profile_preferences_spec.js
@@ -1,27 +1,58 @@
+import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-
-import ProfilePreferences from '~/profile/preferences/components/profile_preferences.vue';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import IntegrationView from '~/profile/preferences/components/integration_view.vue';
-import { integrationViews, userFields } from '../mock_data';
+import ProfilePreferences from '~/profile/preferences/components/profile_preferences.vue';
+import { i18n } from '~/profile/preferences/constants';
+import { integrationViews, userFields, bodyClasses } from '../mock_data';
+
+const expectedUrl = '/foo';
describe('ProfilePreferences component', () => {
let wrapper;
const defaultProvide = {
integrationViews: [],
userFields,
+ bodyClasses,
+ themes: [{ id: 1, css_class: 'foo' }],
+ profilePreferencesPath: '/update-profile',
+ formEl: document.createElement('form'),
};
function createComponent(options = {}) {
- const { props = {}, provide = {} } = options;
- return shallowMount(ProfilePreferences, {
- provide: {
- ...defaultProvide,
- ...provide,
- },
- propsData: props,
- });
+ const { props = {}, provide = {}, attachTo } = options;
+ return extendedWrapper(
+ shallowMount(ProfilePreferences, {
+ provide: {
+ ...defaultProvide,
+ ...provide,
+ },
+ propsData: props,
+ attachTo,
+ }),
+ );
+ }
+
+ function findIntegrationsDivider() {
+ return wrapper.findByTestId('profile-preferences-integrations-rule');
+ }
+
+ function findIntegrationsHeading() {
+ return wrapper.findByTestId('profile-preferences-integrations-heading');
+ }
+
+ function findSubmitButton() {
+ return wrapper.findComponent(GlButton);
}
+ function findFlashError() {
+ return document.querySelector('.flash-container .flash-text');
+ }
+
+ beforeEach(() => {
+ setFixtures('<div class="flash-container"></div>');
+ });
+
afterEach(() => {
wrapper.destroy();
wrapper = null;
@@ -30,8 +61,8 @@ describe('ProfilePreferences component', () => {
it('should not render Integrations section', () => {
wrapper = createComponent();
const views = wrapper.findAll(IntegrationView);
- const divider = wrapper.find('[data-testid="profile-preferences-integrations-rule"]');
- const heading = wrapper.find('[data-testid="profile-preferences-integrations-heading"]');
+ const divider = findIntegrationsDivider();
+ const heading = findIntegrationsHeading();
expect(divider.exists()).toBe(false);
expect(heading.exists()).toBe(false);
@@ -40,8 +71,8 @@ describe('ProfilePreferences component', () => {
it('should render Integration section', () => {
wrapper = createComponent({ provide: { integrationViews } });
- const divider = wrapper.find('[data-testid="profile-preferences-integrations-rule"]');
- const heading = wrapper.find('[data-testid="profile-preferences-integrations-heading"]');
+ const divider = findIntegrationsDivider();
+ const heading = findIntegrationsHeading();
const views = wrapper.findAll(IntegrationView);
expect(divider.exists()).toBe(true);
@@ -49,9 +80,84 @@ describe('ProfilePreferences component', () => {
expect(views).toHaveLength(integrationViews.length);
});
- it('should render ProfilePreferences properly', () => {
- wrapper = createComponent({ provide: { integrationViews } });
+ describe('form submit', () => {
+ let form;
- expect(wrapper.element).toMatchSnapshot();
+ beforeEach(() => {
+ const div = document.createElement('div');
+ div.classList.add('container-fluid');
+ document.body.appendChild(div);
+ document.body.classList.add('content-wrapper');
+
+ form = document.createElement('form');
+ form.setAttribute('url', expectedUrl);
+ form.setAttribute('method', 'put');
+
+ const input = document.createElement('input');
+ input.setAttribute('name', 'user[theme_id]');
+ input.setAttribute('type', 'radio');
+ input.setAttribute('value', '1');
+ input.setAttribute('checked', 'checked');
+ form.appendChild(input);
+
+ wrapper = createComponent({ provide: { formEl: form }, attachTo: document.body });
+
+ const beforeSendEvent = new CustomEvent('ajax:beforeSend');
+ form.dispatchEvent(beforeSendEvent);
+ });
+
+ it('disables the submit button', async () => {
+ await wrapper.vm.$nextTick();
+ const button = findSubmitButton();
+ expect(button.props('disabled')).toBe(true);
+ });
+
+ it('success re-enables the submit button', async () => {
+ const successEvent = new CustomEvent('ajax:success');
+ form.dispatchEvent(successEvent);
+
+ await wrapper.vm.$nextTick();
+ const button = findSubmitButton();
+ expect(button.props('disabled')).toBe(false);
+ });
+
+ it('error re-enables the submit button', async () => {
+ const errorEvent = new CustomEvent('ajax:error');
+ form.dispatchEvent(errorEvent);
+
+ await wrapper.vm.$nextTick();
+ const button = findSubmitButton();
+ expect(button.props('disabled')).toBe(false);
+ });
+
+ it('displays the default success message', () => {
+ const successEvent = new CustomEvent('ajax:success');
+ form.dispatchEvent(successEvent);
+
+ expect(findFlashError().innerText.trim()).toEqual(i18n.defaultSuccess);
+ });
+
+ it('displays the custom success message', () => {
+ const message = 'foo';
+ const successEvent = new CustomEvent('ajax:success', { detail: [{ message }] });
+ form.dispatchEvent(successEvent);
+
+ expect(findFlashError().innerText.trim()).toEqual(message);
+ });
+
+ it('displays the default error message', () => {
+ const errorEvent = new CustomEvent('ajax:error');
+ form.dispatchEvent(errorEvent);
+
+ expect(findFlashError().innerText.trim()).toEqual(i18n.defaultError);
+ });
+
+ it('displays the custom error message', () => {
+ const message = 'bar';
+ const errorEvent = new CustomEvent('ajax:error', { detail: [{ message }] });
+ form.dispatchEvent(errorEvent);
+
+ expect(findFlashError().innerText.trim()).toEqual(message);
+ });
});
});
diff --git a/spec/frontend/profile/preferences/mock_data.js b/spec/frontend/profile/preferences/mock_data.js
index d07d5f565dc..ce33fc79a39 100644
--- a/spec/frontend/profile/preferences/mock_data.js
+++ b/spec/frontend/profile/preferences/mock_data.js
@@ -16,3 +16,5 @@ export const integrationViews = [
export const userFields = {
foo_enabled: true,
};
+
+export const bodyClasses = 'ui-light-indigo ui-light gl-dark';
diff --git a/spec/frontend/project_find_file_spec.js b/spec/frontend/project_find_file_spec.js
index 1af97dbca0a..5919910d791 100644
--- a/spec/frontend/project_find_file_spec.js
+++ b/spec/frontend/project_find_file_spec.js
@@ -2,8 +2,8 @@ import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import { TEST_HOST } from 'helpers/test_constants';
import { sanitize } from '~/lib/dompurify';
-import ProjectFindFile from '~/project_find_file';
import axios from '~/lib/utils/axios_utils';
+import ProjectFindFile from '~/project_find_file';
jest.mock('~/lib/dompurify', () => ({
addHook: jest.fn(),
diff --git a/spec/frontend/projects/commit/components/branches_dropdown_spec.js b/spec/frontend/projects/commit/components/branches_dropdown_spec.js
index 9fa7d658405..7686c28c7fc 100644
--- a/spec/frontend/projects/commit/components/branches_dropdown_spec.js
+++ b/spec/frontend/projects/commit/components/branches_dropdown_spec.js
@@ -1,8 +1,8 @@
+import { GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
-import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import { GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
import BranchesDropdown from '~/projects/commit/components/branches_dropdown.vue';
Vue.use(Vuex);
diff --git a/spec/frontend/projects/commit/components/form_modal_spec.js b/spec/frontend/projects/commit/components/form_modal_spec.js
index 1c37b82fed3..1569f5b4bbe 100644
--- a/spec/frontend/projects/commit/components/form_modal_spec.js
+++ b/spec/frontend/projects/commit/components/form_modal_spec.js
@@ -1,12 +1,13 @@
-import MockAdapter from 'axios-mock-adapter';
-import { shallowMount, mount, createWrapper } from '@vue/test-utils';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { GlModal, GlForm, GlFormCheckbox, GlSprintf } from '@gitlab/ui';
import { within } from '@testing-library/dom';
+import { shallowMount, mount, createWrapper } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import axios from '~/lib/utils/axios_utils';
-import eventHub from '~/projects/commit/event_hub';
-import CommitFormModal from '~/projects/commit/components/form_modal.vue';
+import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import BranchesDropdown from '~/projects/commit/components/branches_dropdown.vue';
+import CommitFormModal from '~/projects/commit/components/form_modal.vue';
+import eventHub from '~/projects/commit/event_hub';
import createStore from '~/projects/commit/store';
import mockData from '../mock_data';
@@ -64,7 +65,7 @@ describe('CommitFormModal', () => {
wrapper.vm.show();
- expect(rootEmit).toHaveBeenCalledWith('bv::show::modal', mockData.modalPropsData.modalId);
+ expect(rootEmit).toHaveBeenCalledWith(BV_SHOW_MODAL, mockData.modalPropsData.modalId);
});
it('Clears the modal state once modal is hidden', () => {
diff --git a/spec/frontend/projects/commit/components/form_trigger_spec.js b/spec/frontend/projects/commit/components/form_trigger_spec.js
index ca51419d6a5..4503493c0a6 100644
--- a/spec/frontend/projects/commit/components/form_trigger_spec.js
+++ b/spec/frontend/projects/commit/components/form_trigger_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import FormTrigger from '~/projects/commit/components/form_trigger.vue';
import eventHub from '~/projects/commit/event_hub';
diff --git a/spec/frontend/projects/commit/store/actions_spec.js b/spec/frontend/projects/commit/store/actions_spec.js
index ec528d4ee88..458372229cf 100644
--- a/spec/frontend/projects/commit/store/actions_spec.js
+++ b/spec/frontend/projects/commit/store/actions_spec.js
@@ -1,12 +1,12 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
-import getInitialState from '~/projects/commit/store/state';
+import axios from '~/lib/utils/axios_utils';
+import { PROJECT_BRANCHES_ERROR } from '~/projects/commit/constants';
import * as actions from '~/projects/commit/store/actions';
import * as types from '~/projects/commit/store/mutation_types';
+import getInitialState from '~/projects/commit/store/state';
import mockData from '../mock_data';
-import { PROJECT_BRANCHES_ERROR } from '~/projects/commit/constants';
jest.mock('~/flash.js');
diff --git a/spec/frontend/projects/commit/store/mutations_spec.js b/spec/frontend/projects/commit/store/mutations_spec.js
index 59ab3d9a74a..2ea50e71772 100644
--- a/spec/frontend/projects/commit/store/mutations_spec.js
+++ b/spec/frontend/projects/commit/store/mutations_spec.js
@@ -1,5 +1,5 @@
-import mutations from '~/projects/commit/store/mutations';
import * as types from '~/projects/commit/store/mutation_types';
+import mutations from '~/projects/commit/store/mutations';
describe('Commit form modal mutations', () => {
let stateCopy;
diff --git a/spec/frontend/projects/commit_box/info/load_branches_spec.js b/spec/frontend/projects/commit_box/info/load_branches_spec.js
index ebd4ee45dab..8100200cbdd 100644
--- a/spec/frontend/projects/commit_box/info/load_branches_spec.js
+++ b/spec/frontend/projects/commit_box/info/load_branches_spec.js
@@ -1,6 +1,6 @@
import axios from 'axios';
-import waitForPromises from 'helpers/wait_for_promises';
import MockAdapter from 'axios-mock-adapter';
+import waitForPromises from 'helpers/wait_for_promises';
import { loadBranches } from '~/projects/commit_box/info/load_branches';
const mockCommitPath = '/commit/abcd/branches';
diff --git a/spec/frontend/projects/commits/components/author_select_spec.js b/spec/frontend/projects/commits/components/author_select_spec.js
index 63920ddfd72..9a8f7ff7582 100644
--- a/spec/frontend/projects/commits/components/author_select_spec.js
+++ b/spec/frontend/projects/commits/components/author_select_spec.js
@@ -1,6 +1,6 @@
+import { GlDropdown, GlDropdownSectionHeader, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import { GlDropdown, GlDropdownSectionHeader, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui';
import * as urlUtility from '~/lib/utils/url_utility';
import AuthorSelect from '~/projects/commits/components/author_select.vue';
import { createStore } from '~/projects/commits/store';
diff --git a/spec/frontend/projects/commits/store/actions_spec.js b/spec/frontend/projects/commits/store/actions_spec.js
index a842aaa2a76..e2c993b8395 100644
--- a/spec/frontend/projects/commits/store/actions_spec.js
+++ b/spec/frontend/projects/commits/store/actions_spec.js
@@ -1,10 +1,10 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import * as types from '~/projects/commits/store/mutation_types';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import actions from '~/projects/commits/store/actions';
+import * as types from '~/projects/commits/store/mutation_types';
import createState from '~/projects/commits/store/state';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
jest.mock('~/flash');
diff --git a/spec/frontend/projects/compare/components/app_spec.js b/spec/frontend/projects/compare/components/app_spec.js
new file mode 100644
index 00000000000..d28a30e93b1
--- /dev/null
+++ b/spec/frontend/projects/compare/components/app_spec.js
@@ -0,0 +1,116 @@
+import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import CompareApp from '~/projects/compare/components/app.vue';
+import RevisionDropdown from '~/projects/compare/components/revision_dropdown.vue';
+
+jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
+
+const projectCompareIndexPath = 'some/path';
+const refsProjectPath = 'some/refs/path';
+const paramsFrom = 'master';
+const paramsTo = 'master';
+
+describe('CompareApp component', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(CompareApp, {
+ propsData: {
+ projectCompareIndexPath,
+ refsProjectPath,
+ paramsFrom,
+ paramsTo,
+ projectMergeRequestPath: '',
+ createMrPath: '',
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders component with prop', () => {
+ expect(wrapper.props()).toEqual(
+ expect.objectContaining({
+ projectCompareIndexPath,
+ refsProjectPath,
+ paramsFrom,
+ paramsTo,
+ }),
+ );
+ });
+
+ it('contains the correct form attributes', () => {
+ expect(wrapper.attributes('action')).toBe(projectCompareIndexPath);
+ expect(wrapper.attributes('method')).toBe('POST');
+ });
+
+ it('has input with csrf token', () => {
+ expect(wrapper.find('input[name="authenticity_token"]').attributes('value')).toBe(
+ 'mock-csrf-token',
+ );
+ });
+
+ it('has ellipsis', () => {
+ expect(wrapper.find('[data-testid="ellipsis"]').exists()).toBe(true);
+ });
+
+ it('render Source and Target BranchDropdown components', () => {
+ const branchDropdowns = wrapper.findAll(RevisionDropdown);
+
+ expect(branchDropdowns.length).toBe(2);
+ expect(branchDropdowns.at(0).props('revisionText')).toBe('Source');
+ expect(branchDropdowns.at(1).props('revisionText')).toBe('Target');
+ });
+
+ describe('compare button', () => {
+ const findCompareButton = () => wrapper.find(GlButton);
+
+ it('renders button', () => {
+ expect(findCompareButton().exists()).toBe(true);
+ });
+
+ it('submits form', () => {
+ findCompareButton().vm.$emit('click');
+ expect(wrapper.find('form').element.submit).toHaveBeenCalled();
+ });
+
+ it('has compare text', () => {
+ expect(findCompareButton().text()).toBe('Compare');
+ });
+ });
+
+ describe('merge request buttons', () => {
+ const findProjectMrButton = () => wrapper.find('[data-testid="projectMrButton"]');
+ const findCreateMrButton = () => wrapper.find('[data-testid="createMrButton"]');
+
+ it('does not have merge request buttons', () => {
+ createComponent();
+ expect(findProjectMrButton().exists()).toBe(false);
+ expect(findCreateMrButton().exists()).toBe(false);
+ });
+
+ it('has "View open merge request" button', () => {
+ createComponent({
+ projectMergeRequestPath: 'some/project/merge/request/path',
+ });
+ expect(findProjectMrButton().exists()).toBe(true);
+ expect(findCreateMrButton().exists()).toBe(false);
+ });
+
+ it('has "Create merge request" button', () => {
+ createComponent({
+ createMrPath: 'some/create/create/mr/path',
+ });
+ expect(findProjectMrButton().exists()).toBe(false);
+ expect(findCreateMrButton().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/projects/compare/components/revision_dropdown_spec.js b/spec/frontend/projects/compare/components/revision_dropdown_spec.js
new file mode 100644
index 00000000000..f3ff5e26d2b
--- /dev/null
+++ b/spec/frontend/projects/compare/components/revision_dropdown_spec.js
@@ -0,0 +1,92 @@
+import { GlDropdown } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import AxiosMockAdapter from 'axios-mock-adapter';
+import createFlash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import RevisionDropdown from '~/projects/compare/components/revision_dropdown.vue';
+
+const defaultProps = {
+ refsProjectPath: 'some/refs/path',
+ revisionText: 'Target',
+ paramsName: 'from',
+ paramsBranch: 'master',
+};
+
+jest.mock('~/flash');
+
+describe('RevisionDropdown component', () => {
+ let wrapper;
+ let axiosMock;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(RevisionDropdown, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ axiosMock = new AxiosMockAdapter(axios);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ axiosMock.restore();
+ });
+
+ const findGlDropdown = () => wrapper.find(GlDropdown);
+
+ it('sets hidden input', () => {
+ createComponent();
+ expect(wrapper.find('input[type="hidden"]').attributes('value')).toBe(
+ defaultProps.paramsBranch,
+ );
+ });
+
+ it('update the branches on success', async () => {
+ const Branches = ['branch-1', 'branch-2'];
+ const Tags = ['tag-1', 'tag-2', 'tag-3'];
+
+ axiosMock.onGet(defaultProps.refsProjectPath).replyOnce(200, {
+ Branches,
+ Tags,
+ });
+
+ createComponent();
+
+ await axios.waitForAll();
+
+ expect(wrapper.vm.branches).toEqual(Branches);
+ expect(wrapper.vm.tags).toEqual(Tags);
+ });
+
+ it('shows flash message on error', async () => {
+ axiosMock.onGet('some/invalid/path').replyOnce(404);
+
+ createComponent();
+
+ await wrapper.vm.fetchBranchesAndTags();
+ expect(createFlash).toHaveBeenCalled();
+ });
+
+ describe('GlDropdown component', () => {
+ it('renders props', () => {
+ createComponent();
+ expect(wrapper.props()).toEqual(expect.objectContaining(defaultProps));
+ });
+
+ it('display default text', () => {
+ createComponent({
+ paramsBranch: null,
+ });
+ expect(findGlDropdown().props('text')).toBe('Select branch/tag');
+ });
+
+ it('display params branch text', () => {
+ createComponent();
+ expect(findGlDropdown().props('text')).toBe(defaultProps.paramsBranch);
+ });
+ });
+});
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 0b9f095a700..f0d72124379 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
@@ -53,12 +53,12 @@ exports[`Project remove modal initialized matches the snapshot 1`] = `
variant="danger"
>
<gl-sprintf-stub
- message="Once a project is permanently deleted it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its repositories and %{strongStart}all related resources%{strongEnd} including issues, merge requests etc."
+ message="Once a project is permanently deleted, it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its repositories and %{strongStart}all related resources%{strongEnd}, including issues, merge requests etc."
/>
</gl-alert-stub>
<p>
- This action cannot be undone. You will lose this project's repository and all content: issues, merge requests, etc.
+ This action cannot be undone. You will lose this project's repository and all related resources, including issues, merge requests, etc.
</p>
<p
diff --git a/spec/frontend/projects/components/shared/delete_button_spec.js b/spec/frontend/projects/components/shared/delete_button_spec.js
index cf7e41a2df2..3e491584670 100644
--- a/spec/frontend/projects/components/shared/delete_button_spec.js
+++ b/spec/frontend/projects/components/shared/delete_button_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { stubComponent } from 'helpers/stub_component';
import SharedDeleteButton from '~/projects/components/shared/delete_button.vue';
diff --git a/spec/frontend/projects/experiment_new_project_creation/components/app_spec.js b/spec/frontend/projects/experiment_new_project_creation/components/app_spec.js
index 9a5f200f5a9..b4ae50341d4 100644
--- a/spec/frontend/projects/experiment_new_project_creation/components/app_spec.js
+++ b/spec/frontend/projects/experiment_new_project_creation/components/app_spec.js
@@ -1,8 +1,8 @@
-import { shallowMount } from '@vue/test-utils';
import { GlBreadcrumb } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import App from '~/projects/experiment_new_project_creation/components/app.vue';
-import WelcomePage from '~/projects/experiment_new_project_creation/components/welcome.vue';
import LegacyContainer from '~/projects/experiment_new_project_creation/components/legacy_container.vue';
+import WelcomePage from '~/projects/experiment_new_project_creation/components/welcome.vue';
describe('Experimental new project creation app', () => {
let wrapper;
diff --git a/spec/frontend/projects/members/utils_spec.js b/spec/frontend/projects/members/utils_spec.js
new file mode 100644
index 00000000000..813e8455e85
--- /dev/null
+++ b/spec/frontend/projects/members/utils_spec.js
@@ -0,0 +1,14 @@
+import { projectMemberRequestFormatter } from '~/projects/members/utils';
+
+describe('project member utils', () => {
+ describe('projectMemberRequestFormatter', () => {
+ it('returns expected format', () => {
+ expect(
+ projectMemberRequestFormatter({
+ accessLevel: 50,
+ expires_at: '2020-10-16',
+ }),
+ ).toEqual({ project_member: { access_level: 50, expires_at: '2020-10-16' } });
+ });
+ });
+});
diff --git a/spec/frontend/projects/pipelines/charts/components/app_spec.js b/spec/frontend/projects/pipelines/charts/components/app_spec.js
index 44329944097..e8aace14db4 100644
--- a/spec/frontend/projects/pipelines/charts/components/app_spec.js
+++ b/spec/frontend/projects/pipelines/charts/components/app_spec.js
@@ -1,32 +1,19 @@
-import { merge } from 'lodash';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
-import VueApollo from 'vue-apollo';
import { GlTabs, GlTab } from '@gitlab/ui';
-import createMockApollo from 'helpers/mock_apollo_helper';
+import { shallowMount } from '@vue/test-utils';
+import { merge } from 'lodash';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import { TEST_HOST } from 'helpers/test_constants';
+import { mergeUrlParams, updateHistory, getParameterValues } from '~/lib/utils/url_utility';
import Component from '~/projects/pipelines/charts/components/app.vue';
import PipelineCharts from '~/projects/pipelines/charts/components/pipeline_charts.vue';
-import getPipelineCountByStatus from '~/projects/pipelines/charts/graphql/queries/get_pipeline_count_by_status.query.graphql';
-import getProjectPipelineStatistics from '~/projects/pipelines/charts/graphql/queries/get_project_pipeline_statistics.query.graphql';
-import { mockPipelineCount, mockPipelineStatistics } from '../mock_data';
-const projectPath = 'gitlab-org/gitlab';
-const localVue = createLocalVue();
-localVue.use(VueApollo);
+jest.mock('~/lib/utils/url_utility');
const DeploymentFrequencyChartsStub = { name: 'DeploymentFrequencyCharts', render: () => {} };
describe('ProjectsPipelinesChartsApp', () => {
let wrapper;
- function createMockApolloProvider() {
- const requestHandlers = [
- [getPipelineCountByStatus, jest.fn().mockResolvedValue(mockPipelineCount)],
- [getProjectPipelineStatistics, jest.fn().mockResolvedValue(mockPipelineStatistics)],
- ];
-
- return createMockApollo(requestHandlers);
- }
-
function createComponent(mountOptions = {}) {
wrapper = shallowMount(
Component,
@@ -34,11 +21,8 @@ describe('ProjectsPipelinesChartsApp', () => {
{},
{
provide: {
- projectPath,
shouldRenderDeploymentFrequencyCharts: false,
},
- localVue,
- apolloProvider: createMockApolloProvider(),
stubs: {
DeploymentFrequencyCharts: DeploymentFrequencyChartsStub,
},
@@ -57,52 +41,15 @@ describe('ProjectsPipelinesChartsApp', () => {
wrapper = null;
});
- describe('pipelines charts', () => {
- it('displays the pipeline charts', () => {
- const chart = wrapper.find(PipelineCharts);
- const analytics = mockPipelineStatistics.data.project.pipelineAnalytics;
-
- const {
- totalPipelines: total,
- successfulPipelines: success,
- failedPipelines: failed,
- } = mockPipelineCount.data.project;
-
- expect(chart.exists()).toBe(true);
- expect(chart.props()).toMatchObject({
- counts: {
- failed: failed.count,
- success: success.count,
- total: total.count,
- successRatio: (success.count / (success.count + failed.count)) * 100,
- },
- lastWeek: {
- labels: analytics.weekPipelinesLabels,
- totals: analytics.weekPipelinesTotals,
- success: analytics.weekPipelinesSuccessful,
- },
- lastMonth: {
- labels: analytics.monthPipelinesLabels,
- totals: analytics.monthPipelinesTotals,
- success: analytics.monthPipelinesSuccessful,
- },
- lastYear: {
- labels: analytics.yearPipelinesLabels,
- totals: analytics.yearPipelinesTotals,
- success: analytics.yearPipelinesSuccessful,
- },
- timesChart: {
- labels: analytics.pipelineTimesLabels,
- values: analytics.pipelineTimesValues,
- },
- });
- });
- });
-
- const findDeploymentFrequencyCharts = () => wrapper.find(DeploymentFrequencyChartsStub);
const findGlTabs = () => wrapper.find(GlTabs);
const findAllGlTab = () => wrapper.findAll(GlTab);
const findGlTabAt = (i) => findAllGlTab().at(i);
+ const findDeploymentFrequencyCharts = () => wrapper.find(DeploymentFrequencyChartsStub);
+ const findPipelineCharts = () => wrapper.find(PipelineCharts);
+
+ it('renders the pipeline charts', () => {
+ expect(findPipelineCharts().exists()).toBe(true);
+ });
describe('when shouldRenderDeploymentFrequencyCharts is true', () => {
beforeEach(() => {
@@ -115,6 +62,97 @@ describe('ProjectsPipelinesChartsApp', () => {
expect(findGlTabAt(1).attributes('title')).toBe('Deployments');
expect(findDeploymentFrequencyCharts().exists()).toBe(true);
});
+
+ it('sets the tab and url when a tab is clicked', async () => {
+ let chartsPath;
+ setWindowLocation(`${TEST_HOST}/gitlab-org/gitlab-test/-/pipelines/charts`);
+
+ mergeUrlParams.mockImplementation(({ chart }, path) => {
+ expect(chart).toBe('deployments');
+ expect(path).toBe(window.location.pathname);
+ chartsPath = `${path}?chart=${chart}`;
+ return chartsPath;
+ });
+
+ updateHistory.mockImplementation(({ url }) => {
+ expect(url).toBe(chartsPath);
+ });
+ const tabs = findGlTabs();
+
+ expect(tabs.attributes('value')).toBe('0');
+
+ tabs.vm.$emit('input', 1);
+
+ await wrapper.vm.$nextTick();
+
+ expect(tabs.attributes('value')).toBe('1');
+ });
+
+ it('should not try to push history if the tab does not change', async () => {
+ setWindowLocation(`${TEST_HOST}/gitlab-org/gitlab-test/-/pipelines/charts`);
+
+ mergeUrlParams.mockImplementation(({ chart }, path) => `${path}?chart=${chart}`);
+
+ const tabs = findGlTabs();
+
+ expect(tabs.attributes('value')).toBe('0');
+
+ tabs.vm.$emit('input', 0);
+
+ await wrapper.vm.$nextTick();
+
+ expect(updateHistory).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when provided with a query param', () => {
+ it.each`
+ chart | tab
+ ${'deployments'} | ${'1'}
+ ${'pipelines'} | ${'0'}
+ ${'fake'} | ${'0'}
+ ${''} | ${'0'}
+ `('shows the correct tab for URL parameter "$chart"', ({ chart, tab }) => {
+ setWindowLocation(`${TEST_HOST}/gitlab-org/gitlab-test/-/pipelines/charts?chart=${chart}`);
+ getParameterValues.mockImplementation((name) => {
+ expect(name).toBe('chart');
+ return chart ? [chart] : [];
+ });
+ createComponent({ provide: { shouldRenderDeploymentFrequencyCharts: true } });
+ expect(findGlTabs().attributes('value')).toBe(tab);
+ });
+
+ it('should set the tab when the back button is clicked', async () => {
+ let popstateHandler;
+
+ window.addEventListener = jest.fn();
+
+ window.addEventListener.mockImplementation((event, handler) => {
+ if (event === 'popstate') {
+ popstateHandler = handler;
+ }
+ });
+
+ getParameterValues.mockImplementation((name) => {
+ expect(name).toBe('chart');
+ return [];
+ });
+
+ createComponent({ provide: { shouldRenderDeploymentFrequencyCharts: true } });
+
+ expect(findGlTabs().attributes('value')).toBe('0');
+
+ getParameterValues.mockImplementationOnce((name) => {
+ expect(name).toBe('chart');
+ return ['deployments'];
+ });
+
+ popstateHandler();
+
+ await wrapper.vm.$nextTick();
+
+ expect(findGlTabs().attributes('value')).toBe('1');
+ });
});
describe('when shouldRenderDeploymentFrequencyCharts is false', () => {
diff --git a/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js b/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js
new file mode 100644
index 00000000000..037530ddd48
--- /dev/null
+++ b/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js
@@ -0,0 +1,94 @@
+import { GlSegmentedControl } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import CiCdAnalyticsAreaChart from '~/projects/pipelines/charts/components/ci_cd_analytics_area_chart.vue';
+import CiCdAnalyticsCharts from '~/projects/pipelines/charts/components/ci_cd_analytics_charts.vue';
+import { transformedAreaChartData, chartOptions } from '../mock_data';
+
+const DEFAULT_PROPS = {
+ chartOptions,
+ charts: [
+ {
+ range: 'test range 1',
+ title: 'title 1',
+ data: transformedAreaChartData,
+ },
+ {
+ range: 'test range 2',
+ title: 'title 2',
+ data: transformedAreaChartData,
+ },
+ {
+ range: 'test range 3',
+ title: 'title 3',
+ data: transformedAreaChartData,
+ },
+ ],
+};
+
+describe('~/projects/pipelines/charts/components/ci_cd_analytics_charts.vue', () => {
+ let wrapper;
+
+ const createWrapper = (props = {}) =>
+ shallowMount(CiCdAnalyticsCharts, {
+ propsData: {
+ ...DEFAULT_PROPS,
+ ...props,
+ },
+ });
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ wrapper = null;
+ }
+ });
+
+ describe('segmented control', () => {
+ let segmentedControl;
+
+ beforeEach(() => {
+ wrapper = createWrapper();
+ segmentedControl = wrapper.find(GlSegmentedControl);
+ });
+
+ it('should default to the first chart', () => {
+ expect(segmentedControl.props('checked')).toBe(0);
+ });
+
+ it('should use the title and index as values', () => {
+ const options = segmentedControl.props('options');
+ expect(options).toHaveLength(3);
+ expect(options).toEqual([
+ {
+ text: 'title 1',
+ value: 0,
+ },
+ {
+ text: 'title 2',
+ value: 1,
+ },
+ {
+ text: 'title 3',
+ value: 2,
+ },
+ ]);
+ });
+
+ it('should select a different chart on change', async () => {
+ segmentedControl.vm.$emit('input', 1);
+
+ const chart = wrapper.find(CiCdAnalyticsAreaChart);
+
+ await nextTick();
+
+ expect(chart.props('chartData')).toEqual(transformedAreaChartData);
+ expect(chart.text()).toBe('Date range: test range 2');
+ });
+ });
+
+ it('should not display charts if there are no charts', () => {
+ wrapper = createWrapper({ charts: [] });
+ expect(wrapper.find(CiCdAnalyticsAreaChart).exists()).toBe(false);
+ });
+});
diff --git a/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js b/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js
index 598055d5828..c5cfe783569 100644
--- a/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js
+++ b/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js
@@ -1,35 +1,37 @@
-import { shallowMount } from '@vue/test-utils';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
-import StatisticsList from '~/projects/pipelines/charts/components/statistics_list.vue';
-import CiCdAnalyticsAreaChart from '~/projects/pipelines/charts/components/ci_cd_analytics_area_chart.vue';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import CiCdAnalyticsCharts from '~/projects/pipelines/charts/components/ci_cd_analytics_charts.vue';
import PipelineCharts from '~/projects/pipelines/charts/components/pipeline_charts.vue';
-import {
- counts,
- timesChartData as timesChart,
- areaChartData as lastWeek,
- areaChartData as lastMonth,
- lastYearChartData as lastYear,
-} from '../mock_data';
+import StatisticsList from '~/projects/pipelines/charts/components/statistics_list.vue';
+import getPipelineCountByStatus from '~/projects/pipelines/charts/graphql/queries/get_pipeline_count_by_status.query.graphql';
+import getProjectPipelineStatistics from '~/projects/pipelines/charts/graphql/queries/get_project_pipeline_statistics.query.graphql';
+import { mockPipelineCount, mockPipelineStatistics } from '../mock_data';
+
+const projectPath = 'gitlab-org/gitlab';
+const localVue = createLocalVue();
+localVue.use(VueApollo);
-describe('ProjectsPipelinesChartsApp', () => {
+describe('~/projects/pipelines/charts/components/pipeline_charts.vue', () => {
let wrapper;
+ function createMockApolloProvider() {
+ const requestHandlers = [
+ [getPipelineCountByStatus, jest.fn().mockResolvedValue(mockPipelineCount)],
+ [getProjectPipelineStatistics, jest.fn().mockResolvedValue(mockPipelineStatistics)],
+ ];
+
+ return createMockApollo(requestHandlers);
+ }
+
beforeEach(() => {
wrapper = shallowMount(PipelineCharts, {
- propsData: {
- counts,
- timesChart,
- lastWeek,
- lastMonth,
- lastYear,
- },
provide: {
- projectPath: 'test/project',
- shouldRenderDeploymentFrequencyCharts: true,
- },
- stubs: {
- DeploymentFrequencyCharts: true,
+ projectPath,
},
+ localVue,
+ apolloProvider: createMockApolloProvider(),
});
});
@@ -43,7 +45,12 @@ describe('ProjectsPipelinesChartsApp', () => {
const list = wrapper.find(StatisticsList);
expect(list.exists()).toBe(true);
- expect(list.props('counts')).toBe(counts);
+ expect(list.props('counts')).toEqual({
+ total: 34,
+ success: 23,
+ failed: 1,
+ successRatio: (23 / (23 + 1)) * 100,
+ });
});
it('displays the commit duration chart', () => {
@@ -58,20 +65,17 @@ describe('ProjectsPipelinesChartsApp', () => {
});
describe('pipelines charts', () => {
- it('displays 3 area charts', () => {
- expect(wrapper.findAll(CiCdAnalyticsAreaChart)).toHaveLength(3);
+ it('displays the charts components', () => {
+ expect(wrapper.find(CiCdAnalyticsCharts).exists()).toBe(true);
});
describe('displays individual correctly', () => {
it('renders with the correct data', () => {
- const charts = wrapper.findAll(CiCdAnalyticsAreaChart);
- for (let i = 0; i < charts.length; i += 1) {
- const chart = charts.at(i);
-
- expect(chart.exists()).toBeTruthy();
- expect(chart.props('chartData')).toBe(wrapper.vm.areaCharts[i].data);
- expect(chart.text()).toBe(wrapper.vm.areaCharts[i].title);
- }
+ const charts = wrapper.find(CiCdAnalyticsCharts);
+ expect(charts.props()).toEqual({
+ charts: wrapper.vm.areaCharts,
+ chartOptions: wrapper.vm.$options.areaChartOptions,
+ });
});
});
});
diff --git a/spec/frontend/projects/pipelines/charts/mock_data.js b/spec/frontend/projects/pipelines/charts/mock_data.js
index 3bc09f0b0a0..2e2c594102c 100644
--- a/spec/frontend/projects/pipelines/charts/mock_data.js
+++ b/spec/frontend/projects/pipelines/charts/mock_data.js
@@ -57,6 +57,16 @@ export const mockPipelineCount = {
},
};
+export const chartOptions = {
+ xAxis: {
+ name: 'X axis title',
+ type: 'category',
+ },
+ yAxis: {
+ name: 'Y axis title',
+ },
+};
+
export const mockPipelineStatistics = {
data: {
project: {
diff --git a/spec/frontend/projects/settings/access_dropdown_spec.js b/spec/frontend/projects/settings/access_dropdown_spec.js
index 8a57930ac83..236968a3736 100644
--- a/spec/frontend/projects/settings/access_dropdown_spec.js
+++ b/spec/frontend/projects/settings/access_dropdown_spec.js
@@ -14,7 +14,6 @@ describe('AccessDropdown', () => {
`);
const $dropdown = $('#dummy-dropdown');
$dropdown.data('defaultLabel', defaultLabel);
- gon.features = { deployKeysOnProtectedBranches: true };
const options = {
$dropdown,
accessLevelsData: {
diff --git a/spec/frontend/projects/settings/components/shared_runners_toggle_spec.js b/spec/frontend/projects/settings/components/shared_runners_toggle_spec.js
index 1fac3d07b16..2d6efe7ae83 100644
--- a/spec/frontend/projects/settings/components/shared_runners_toggle_spec.js
+++ b/spec/frontend/projects/settings/components/shared_runners_toggle_spec.js
@@ -2,8 +2,8 @@ import { GlAlert, GlToggle, GlTooltip } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAxiosAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
-import SharedRunnersToggleComponent from '~/projects/settings/components/shared_runners_toggle.vue';
import axios from '~/lib/utils/axios_utils';
+import SharedRunnersToggleComponent from '~/projects/settings/components/shared_runners_toggle.vue';
const TEST_UPDATE_PATH = '/test/update_shared_runners';
diff --git a/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js b/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
index c83b1852147..f9fbb1b3016 100644
--- a/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
+++ b/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
@@ -1,20 +1,36 @@
-import { mount } from '@vue/test-utils';
+import { GlAlert } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
-import ServiceDeskRoot from '~/projects/settings_service_desk/components/service_desk_root.vue';
-import ServiceDeskSetting from '~/projects/settings_service_desk/components/service_desk_setting.vue';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
+import ServiceDeskRoot from '~/projects/settings_service_desk/components/service_desk_root.vue';
+import ServiceDeskSetting from '~/projects/settings_service_desk/components/service_desk_setting.vue';
describe('ServiceDeskRoot', () => {
- const endpoint = '/gitlab-org/gitlab-test/service_desk';
- const initialIncomingEmail = 'servicedeskaddress@example.com';
let axiosMock;
let wrapper;
let spy;
+ const provideData = {
+ customEmail: 'custom.email@example.com',
+ customEmailEnabled: true,
+ endpoint: '/gitlab-org/gitlab-test/service_desk',
+ initialIncomingEmail: 'servicedeskaddress@example.com',
+ initialIsEnabled: true,
+ outgoingName: 'GitLab Support Bot',
+ projectKey: 'key',
+ selectedTemplate: 'Bug',
+ templates: ['Bug', 'Documentation'],
+ };
+
+ const getAlertText = () => wrapper.find(GlAlert).text();
+
+ const createComponent = () => shallowMount(ServiceDeskRoot, { provide: provideData });
+
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
+ spy = jest.spyOn(axios, 'put');
});
afterEach(() => {
@@ -25,156 +41,122 @@ describe('ServiceDeskRoot', () => {
}
});
- it('sends a request to toggle service desk off when the toggle is clicked from the on state', () => {
- axiosMock.onPut(endpoint).replyOnce(httpStatusCodes.OK);
+ describe('ServiceDeskSetting component', () => {
+ it('is rendered', () => {
+ wrapper = createComponent();
+
+ expect(wrapper.find(ServiceDeskSetting).props()).toEqual({
+ customEmail: provideData.customEmail,
+ customEmailEnabled: provideData.customEmailEnabled,
+ incomingEmail: provideData.initialIncomingEmail,
+ initialOutgoingName: provideData.outgoingName,
+ initialProjectKey: provideData.projectKey,
+ initialSelectedTemplate: provideData.selectedTemplate,
+ isEnabled: provideData.initialIsEnabled,
+ isTemplateSaving: false,
+ templates: provideData.templates,
+ });
+ });
+
+ describe('toggle event', () => {
+ describe('when toggling service desk on', () => {
+ beforeEach(async () => {
+ wrapper = createComponent();
- spy = jest.spyOn(axios, 'put');
+ wrapper.find(ServiceDeskSetting).vm.$emit('toggle', true);
- wrapper = mount(ServiceDeskRoot, {
- propsData: {
- initialIsEnabled: true,
- initialIncomingEmail,
- endpoint,
- },
- });
+ await waitForPromises();
+ });
+
+ it('sends a request to turn service desk on', () => {
+ axiosMock.onPut(provideData.endpoint).replyOnce(httpStatusCodes.OK);
- wrapper.find('button.gl-toggle').trigger('click');
+ expect(spy).toHaveBeenCalledWith(provideData.endpoint, { service_desk_enabled: true });
+ });
- return wrapper.vm
- .$nextTick()
- .then(waitForPromises)
- .then(() => {
- expect(spy).toHaveBeenCalledWith(endpoint, { service_desk_enabled: false });
+ it('shows a message when there is an error', () => {
+ axiosMock.onPut(provideData.endpoint).networkError();
+
+ expect(getAlertText()).toContain('An error occurred while enabling Service Desk.');
+ });
});
- });
- it('sends a request to toggle service desk on when the toggle is clicked from the off state', () => {
- axiosMock.onPut(endpoint).replyOnce(httpStatusCodes.OK);
+ describe('when toggling service desk off', () => {
+ beforeEach(async () => {
+ wrapper = createComponent();
- spy = jest.spyOn(axios, 'put');
+ wrapper.find(ServiceDeskSetting).vm.$emit('toggle', false);
- wrapper = mount(ServiceDeskRoot, {
- propsData: {
- initialIsEnabled: false,
- initialIncomingEmail: '',
- endpoint,
- },
- });
+ await waitForPromises();
+ });
- wrapper.find('button.gl-toggle').trigger('click');
+ it('sends a request to turn service desk off', () => {
+ axiosMock.onPut(provideData.endpoint).replyOnce(httpStatusCodes.OK);
- return wrapper.vm.$nextTick(() => {
- expect(spy).toHaveBeenCalledWith(endpoint, { service_desk_enabled: true });
- });
- });
+ expect(spy).toHaveBeenCalledWith(provideData.endpoint, { service_desk_enabled: false });
+ });
- it('shows an error message when there is an issue toggling service desk on', () => {
- axiosMock.onPut(endpoint).networkError();
+ it('shows a message when there is an error', () => {
+ axiosMock.onPut(provideData.endpoint).networkError();
- wrapper = mount(ServiceDeskRoot, {
- propsData: {
- initialIsEnabled: false,
- initialIncomingEmail: '',
- endpoint,
- },
+ expect(getAlertText()).toContain('An error occurred while disabling Service Desk.');
+ });
+ });
});
- wrapper.find('button.gl-toggle').trigger('click');
+ describe('save event', () => {
+ describe('successful request', () => {
+ beforeEach(async () => {
+ axiosMock.onPut(provideData.endpoint).replyOnce(httpStatusCodes.OK);
- return wrapper.vm
- .$nextTick()
- .then(waitForPromises)
- .then(() => {
- expect(wrapper.html()).toContain('An error occurred while enabling Service Desk.');
- });
- });
+ wrapper = createComponent();
- it('sends a request to update template when the "Save template" button is clicked', () => {
- axiosMock.onPut(endpoint).replyOnce(httpStatusCodes.OK);
+ const payload = {
+ selectedTemplate: 'Bug',
+ outgoingName: 'GitLab Support Bot',
+ projectKey: 'key',
+ };
- spy = jest.spyOn(axios, 'put');
+ wrapper.find(ServiceDeskSetting).vm.$emit('save', payload);
- wrapper = mount(ServiceDeskRoot, {
- propsData: {
- initialIsEnabled: true,
- endpoint,
- initialIncomingEmail,
- selectedTemplate: 'Bug',
- outgoingName: 'GitLab Support Bot',
- templates: ['Bug', 'Documentation'],
- projectKey: 'key',
- },
- });
+ await waitForPromises();
+ });
- wrapper.find('button.btn-success').trigger('click');
+ it('sends a request to update template', async () => {
+ expect(spy).toHaveBeenCalledWith(provideData.endpoint, {
+ issue_template_key: 'Bug',
+ outgoing_name: 'GitLab Support Bot',
+ project_key: 'key',
+ service_desk_enabled: true,
+ });
+ });
- return wrapper.vm.$nextTick(() => {
- expect(spy).toHaveBeenCalledWith(endpoint, {
- issue_template_key: 'Bug',
- outgoing_name: 'GitLab Support Bot',
- project_key: 'key',
- service_desk_enabled: true,
+ it('shows success message', () => {
+ expect(getAlertText()).toContain('Changes saved.');
+ });
});
- });
- });
- it('saves the template when the "Save template" button is clicked', () => {
- axiosMock.onPut(endpoint).replyOnce(httpStatusCodes.OK);
-
- wrapper = mount(ServiceDeskRoot, {
- propsData: {
- initialIsEnabled: true,
- endpoint,
- initialIncomingEmail,
- selectedTemplate: 'Bug',
- templates: ['Bug', 'Documentation'],
- },
- });
+ describe('unsuccessful request', () => {
+ beforeEach(async () => {
+ axiosMock.onPut(provideData.endpoint).networkError();
- wrapper.find('button.btn-success').trigger('click');
+ wrapper = createComponent();
- return wrapper.vm
- .$nextTick()
- .then(waitForPromises)
- .then(() => {
- expect(wrapper.html()).toContain('Changes saved.');
- });
- });
+ const payload = {
+ selectedTemplate: 'Bug',
+ outgoingName: 'GitLab Support Bot',
+ projectKey: 'key',
+ };
- it('shows an error message when there is an issue saving the template', () => {
- axiosMock.onPut(endpoint).networkError();
-
- wrapper = mount(ServiceDeskRoot, {
- propsData: {
- initialIsEnabled: true,
- endpoint,
- initialIncomingEmail,
- selectedTemplate: 'Bug',
- templates: ['Bug', 'Documentation'],
- },
- });
+ wrapper.find(ServiceDeskSetting).vm.$emit('save', payload);
- wrapper.find('button.btn-success').trigger('click');
+ await waitForPromises();
+ });
- return wrapper.vm
- .$nextTick()
- .then(waitForPromises)
- .then(() => {
- expect(wrapper.html()).toContain('An error occured while saving changes:');
+ it('shows an error message', () => {
+ expect(getAlertText()).toContain('An error occured while saving changes:');
+ });
});
- });
-
- it('passes customEmail through updatedCustomEmail correctly', () => {
- const customEmail = 'foo';
-
- wrapper = mount(ServiceDeskRoot, {
- propsData: {
- initialIsEnabled: true,
- endpoint,
- customEmail,
- },
});
-
- expect(wrapper.find(ServiceDeskSetting).props('customEmail')).toEqual(customEmail);
});
});
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 ddd9a7b2fad..f6744f4971e 100644
--- a/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
+++ b/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
@@ -1,63 +1,68 @@
+import { GlButton, GlFormSelect, GlLoadingIcon, GlToggle } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
-import { GlLoadingIcon } from '@gitlab/ui';
-import eventHub from '~/projects/settings_service_desk/event_hub';
+import { nextTick } from 'vue';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import ServiceDeskSetting from '~/projects/settings_service_desk/components/service_desk_setting.vue';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
describe('ServiceDeskSetting', () => {
let wrapper;
+ const findButton = () => wrapper.find(GlButton);
+ const findClipboardButton = () => wrapper.find(ClipboardButton);
+ const findIncomingEmail = () => wrapper.findByTestId('incoming-email');
+ const findIncomingEmailLabel = () => wrapper.findByTestId('incoming-email-describer');
+ const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const findTemplateDropdown = () => wrapper.find(GlFormSelect);
+ const findToggle = () => wrapper.find(GlToggle);
+
+ const createComponent = ({ props = {}, mountFunction = shallowMount } = {}) =>
+ extendedWrapper(
+ mountFunction(ServiceDeskSetting, {
+ propsData: {
+ isEnabled: true,
+ ...props,
+ },
+ }),
+ );
+
afterEach(() => {
if (wrapper) {
wrapper.destroy();
}
});
- const findTemplateDropdown = () => wrapper.find('#service-desk-template-select');
- const findIncomingEmail = () => wrapper.find('[data-testid="incoming-email"]');
-
describe('when isEnabled=true', () => {
describe('only isEnabled', () => {
describe('as project admin', () => {
beforeEach(() => {
- wrapper = shallowMount(ServiceDeskSetting, {
- propsData: {
- isEnabled: true,
- },
- });
+ wrapper = createComponent();
});
it('should see activation checkbox', () => {
- expect(wrapper.find('#service-desk-checkbox').exists()).toBe(true);
+ expect(findToggle().exists()).toBe(true);
});
it('should see main panel with the email info', () => {
- expect(wrapper.find('#incoming-email-describer').exists()).toBe(true);
+ expect(findIncomingEmailLabel().exists()).toBe(true);
});
it('should see loading spinner and not the incoming email', () => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(findLoadingIcon().exists()).toBe(true);
expect(findIncomingEmail().exists()).toBe(false);
});
});
});
describe('service desk toggle', () => {
- it('emits an event to turn on Service Desk when clicked', () => {
- const eventSpy = jest.fn();
- eventHub.$on('serviceDeskEnabledCheckboxToggled', eventSpy);
-
- wrapper = mount(ServiceDeskSetting, {
- propsData: {
- isEnabled: false,
- },
- });
+ it('emits an event to turn on Service Desk when clicked', async () => {
+ wrapper = createComponent();
- wrapper.find('#service-desk-checkbox').trigger('click');
+ findToggle().vm.$emit('change', true);
- expect(eventSpy).toHaveBeenCalledWith(true);
+ await nextTick();
- eventHub.$off('serviceDeskEnabledCheckboxToggled', eventSpy);
- eventSpy.mockRestore();
+ expect(wrapper.emitted('toggle')[0]).toEqual([true]);
});
});
@@ -65,23 +70,23 @@ describe('ServiceDeskSetting', () => {
const incomingEmail = 'foo@bar.com';
beforeEach(() => {
- wrapper = mount(ServiceDeskSetting, {
- propsData: {
- isEnabled: true,
- incomingEmail,
- },
+ wrapper = createComponent({
+ props: { incomingEmail },
});
});
it('should see email and not the loading spinner', () => {
expect(findIncomingEmail().element.value).toEqual(incomingEmail);
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(findLoadingIcon().exists()).toBe(false);
});
it('renders a copy to clipboard button', () => {
- expect(wrapper.find('.qa-clipboard-button').exists()).toBe(true);
- expect(wrapper.find('.qa-clipboard-button').element.dataset.clipboardText).toBe(
- incomingEmail,
+ expect(findClipboardButton().exists()).toBe(true);
+ expect(findClipboardButton().props()).toEqual(
+ expect.objectContaining({
+ title: 'Copy',
+ text: incomingEmail,
+ }),
);
});
});
@@ -92,12 +97,8 @@ describe('ServiceDeskSetting', () => {
const customEmail = 'custom@bar.com';
beforeEach(() => {
- wrapper = mount(ServiceDeskSetting, {
- propsData: {
- isEnabled: true,
- incomingEmail,
- customEmail,
- },
+ wrapper = createComponent({
+ props: { incomingEmail, customEmail },
});
});
@@ -110,12 +111,8 @@ describe('ServiceDeskSetting', () => {
const email = 'foo@bar.com';
beforeEach(() => {
- wrapper = mount(ServiceDeskSetting, {
- propsData: {
- isEnabled: true,
- incomingEmail: email,
- customEmail: email,
- },
+ wrapper = createComponent({
+ props: { incomingEmail: email, customEmail: email },
});
});
@@ -127,21 +124,13 @@ describe('ServiceDeskSetting', () => {
describe('templates dropdown', () => {
it('renders a dropdown to choose a template', () => {
- wrapper = shallowMount(ServiceDeskSetting, {
- propsData: {
- isEnabled: true,
- },
- });
+ wrapper = createComponent();
- expect(wrapper.find('#service-desk-template-select').exists()).toBe(true);
+ expect(findTemplateDropdown().exists()).toBe(true);
});
it('renders a dropdown with a default value of ""', () => {
- wrapper = mount(ServiceDeskSetting, {
- propsData: {
- isEnabled: true,
- },
- });
+ wrapper = createComponent({ mountFunction: mount });
expect(findTemplateDropdown().element.value).toEqual('');
});
@@ -149,23 +138,18 @@ describe('ServiceDeskSetting', () => {
it('renders a dropdown with a value of "Bug" when it is the initial value', () => {
const templates = ['Bug', 'Documentation', 'Security release'];
- wrapper = mount(ServiceDeskSetting, {
- propsData: {
- isEnabled: true,
- initialSelectedTemplate: 'Bug',
- templates,
- },
+ wrapper = createComponent({
+ props: { initialSelectedTemplate: 'Bug', templates },
+ mountFunction: mount,
});
expect(findTemplateDropdown().element.value).toEqual('Bug');
});
it('renders a dropdown with no options when the project has no templates', () => {
- wrapper = mount(ServiceDeskSetting, {
- propsData: {
- isEnabled: true,
- templates: [],
- },
+ wrapper = createComponent({
+ props: { templates: [] },
+ mountFunction: mount,
});
// The dropdown by default has one empty option
@@ -174,11 +158,10 @@ describe('ServiceDeskSetting', () => {
it('renders a dropdown with options when the project has templates', () => {
const templates = ['Bug', 'Documentation', 'Security release'];
- wrapper = mount(ServiceDeskSetting, {
- propsData: {
- isEnabled: true,
- templates,
- },
+
+ wrapper = createComponent({
+ props: { templates },
+ mountFunction: mount,
});
// An empty-named template is prepended so the user can select no template
@@ -199,78 +182,59 @@ describe('ServiceDeskSetting', () => {
describe('save button', () => {
it('renders a save button to save a template', () => {
- wrapper = mount(ServiceDeskSetting, {
- propsData: {
- isEnabled: true,
- },
- });
+ wrapper = createComponent();
- expect(wrapper.find('button.btn-success').text()).toContain('Save changes');
+ expect(findButton().text()).toContain('Save changes');
});
- it('emits a save event with the chosen template when the save button is clicked', () => {
- const eventSpy = jest.fn();
- eventHub.$on('serviceDeskTemplateSave', eventSpy);
-
- wrapper = mount(ServiceDeskSetting, {
- propsData: {
- isEnabled: true,
+ it('emits a save event with the chosen template when the save button is clicked', async () => {
+ wrapper = createComponent({
+ props: {
initialSelectedTemplate: 'Bug',
initialOutgoingName: 'GitLab Support Bot',
initialProjectKey: 'key',
},
});
- wrapper.find('button.btn-success').trigger('click');
+ findButton().vm.$emit('click');
+
+ await nextTick();
- expect(eventSpy).toHaveBeenCalledWith({
+ const payload = {
selectedTemplate: 'Bug',
outgoingName: 'GitLab Support Bot',
projectKey: 'key',
- });
+ };
- eventHub.$off('serviceDeskTemplateSave', eventSpy);
- eventSpy.mockRestore();
+ expect(wrapper.emitted('save')[0]).toEqual([payload]);
});
});
describe('when isEnabled=false', () => {
beforeEach(() => {
- wrapper = shallowMount(ServiceDeskSetting, {
- propsData: {
- isEnabled: false,
- },
+ wrapper = createComponent({
+ props: { isEnabled: false },
});
});
it('does not render email panel', () => {
- expect(wrapper.find('#incoming-email-describer').exists()).toBe(false);
+ expect(findIncomingEmailLabel().exists()).toBe(false);
});
it('does not render template dropdown', () => {
- expect(wrapper.find('#service-desk-template-select').exists()).toBe(false);
+ expect(findTemplateDropdown().exists()).toBe(false);
});
it('does not render template save button', () => {
- expect(wrapper.find('button.btn-success').exists()).toBe(false);
+ expect(findButton().exists()).toBe(false);
});
- it('emits an event to turn on Service Desk when the toggle is clicked', () => {
- const eventSpy = jest.fn();
- eventHub.$on('serviceDeskEnabledCheckboxToggled', eventSpy);
-
- wrapper = mount(ServiceDeskSetting, {
- propsData: {
- isEnabled: true,
- },
- });
-
- wrapper.find('#service-desk-checkbox').trigger('click');
+ it('emits an event to turn on Service Desk when the toggle is clicked', async () => {
+ findToggle().vm.$emit('change', false);
- expect(eventSpy).toHaveBeenCalledWith(false);
+ await nextTick();
- eventHub.$off('serviceDeskEnabledCheckboxToggled', eventSpy);
- eventSpy.mockRestore();
+ expect(wrapper.emitted('toggle')[0]).toEqual([false]);
});
});
});
diff --git a/spec/frontend/projects/settings_service_desk/services/service_desk_service_spec.js b/spec/frontend/projects/settings_service_desk/services/service_desk_service_spec.js
deleted file mode 100644
index d5340df03fe..00000000000
--- a/spec/frontend/projects/settings_service_desk/services/service_desk_service_spec.js
+++ /dev/null
@@ -1,111 +0,0 @@
-import AxiosMockAdapter from 'axios-mock-adapter';
-import ServiceDeskService from '~/projects/settings_service_desk/services/service_desk_service';
-import axios from '~/lib/utils/axios_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
-
-describe('ServiceDeskService', () => {
- const endpoint = `/gitlab-org/gitlab-test/service_desk`;
- const dummyResponse = { message: 'Dummy response' };
- const errorMessage = 'Network Error';
- let axiosMock;
- let service;
-
- beforeEach(() => {
- axiosMock = new AxiosMockAdapter(axios);
- service = new ServiceDeskService(endpoint);
- });
-
- afterEach(() => {
- axiosMock.restore();
- });
-
- describe('toggleServiceDesk', () => {
- it('makes a request to set service desk', () => {
- axiosMock.onPut(endpoint).replyOnce(httpStatusCodes.OK, dummyResponse);
-
- return service.toggleServiceDesk(true).then((response) => {
- expect(response.data).toEqual(dummyResponse);
- });
- });
-
- it('fails on error response', () => {
- axiosMock.onPut(endpoint).networkError();
-
- return service.toggleServiceDesk(true).catch((error) => {
- expect(error.message).toBe(errorMessage);
- });
- });
-
- it('makes a request with the expected body', () => {
- axiosMock.onPut(endpoint).replyOnce(httpStatusCodes.OK, dummyResponse);
-
- const spy = jest.spyOn(axios, 'put');
-
- service.toggleServiceDesk(true);
-
- expect(spy).toHaveBeenCalledWith(endpoint, {
- service_desk_enabled: true,
- });
-
- spy.mockRestore();
- });
- });
-
- describe('updateTemplate', () => {
- it('makes a request to update template', () => {
- axiosMock.onPut(endpoint).replyOnce(httpStatusCodes.OK, dummyResponse);
-
- return service
- .updateTemplate(
- {
- selectedTemplate: 'Bug',
- outgoingName: 'GitLab Support Bot',
- },
- true,
- )
- .then((response) => {
- expect(response.data).toEqual(dummyResponse);
- });
- });
-
- it('fails on error response', () => {
- axiosMock.onPut(endpoint).networkError();
-
- return service
- .updateTemplate(
- {
- selectedTemplate: 'Bug',
- outgoingName: 'GitLab Support Bot',
- },
- true,
- )
- .catch((error) => {
- expect(error.message).toBe(errorMessage);
- });
- });
-
- it('makes a request with the expected body', () => {
- axiosMock.onPut(endpoint).replyOnce(httpStatusCodes.OK, dummyResponse);
-
- const spy = jest.spyOn(axios, 'put');
-
- service.updateTemplate(
- {
- selectedTemplate: 'Bug',
- outgoingName: 'GitLab Support Bot',
- projectKey: 'key',
- },
- true,
- );
-
- expect(spy).toHaveBeenCalledWith(endpoint, {
- issue_template_key: 'Bug',
- outgoing_name: 'GitLab Support Bot',
- project_key: 'key',
- service_desk_enabled: true,
- });
-
- spy.mockRestore();
- });
- });
-});
diff --git a/spec/frontend/prometheus_alerts/components/reset_key_spec.js b/spec/frontend/prometheus_alerts/components/reset_key_spec.js
index 489586a60fe..edf5297cc6a 100644
--- a/spec/frontend/prometheus_alerts/components/reset_key_spec.js
+++ b/spec/frontend/prometheus_alerts/components/reset_key_spec.js
@@ -1,10 +1,10 @@
+import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import { GlModal } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
+import axios from '~/lib/utils/axios_utils';
import ResetKey from '~/prometheus_alerts/components/reset_key.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import axios from '~/lib/utils/axios_utils';
describe('ResetKey', () => {
let mock;
diff --git a/spec/frontend/prometheus_metrics/custom_metrics_spec.js b/spec/frontend/prometheus_metrics/custom_metrics_spec.js
index 1244d7342ad..3e3d4ee361a 100644
--- a/spec/frontend/prometheus_metrics/custom_metrics_spec.js
+++ b/spec/frontend/prometheus_metrics/custom_metrics_spec.js
@@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
-import CustomMetrics from '~/prometheus_metrics/custom_metrics';
import axios from '~/lib/utils/axios_utils';
import PANEL_STATE from '~/prometheus_metrics/constants';
+import CustomMetrics from '~/prometheus_metrics/custom_metrics';
import { metrics1 as metrics } from './mock_data';
describe('PrometheusMetrics', () => {
diff --git a/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js b/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
index f6b2780e167..722a5274ad4 100644
--- a/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
+++ b/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
@@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
-import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics';
import PANEL_STATE from '~/prometheus_metrics/constants';
+import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics';
import { metrics2 as metrics, missingVarMetrics } from './mock_data';
describe('PrometheusMetrics', () => {
diff --git a/spec/frontend/ref/components/ref_selector_spec.js b/spec/frontend/ref/components/ref_selector_spec.js
index 7efb6e9ba4a..27ada131ed6 100644
--- a/spec/frontend/ref/components/ref_selector_spec.js
+++ b/spec/frontend/ref/components/ref_selector_spec.js
@@ -1,11 +1,11 @@
-import Vuex from 'vuex';
+import { GlLoadingIcon, GlSearchBoxByType, GlDropdownItem, GlIcon } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
-import { GlLoadingIcon, GlSearchBoxByType, GlDropdownItem, GlIcon } from '@gitlab/ui';
+import Vuex from 'vuex';
import { trimText } from 'helpers/text_helper';
-import { sprintf } from '~/locale';
import { ENTER_KEY } from '~/lib/utils/keys';
+import { sprintf } from '~/locale';
import RefSelector from '~/ref/components/ref_selector.vue';
import { X_TOTAL_HEADER, DEFAULT_I18N } from '~/ref/constants';
import createStore from '~/ref/stores/';
diff --git a/spec/frontend/ref/stores/actions_spec.js b/spec/frontend/ref/stores/actions_spec.js
index 32966354c95..11acec27165 100644
--- a/spec/frontend/ref/stores/actions_spec.js
+++ b/spec/frontend/ref/stores/actions_spec.js
@@ -1,7 +1,7 @@
import testAction from 'helpers/vuex_action_helper';
-import createState from '~/ref/stores/state';
import * as actions from '~/ref/stores/actions';
import * as types from '~/ref/stores/mutation_types';
+import createState from '~/ref/stores/state';
let mockBranchesReturnValue;
let mockTagsReturnValue;
diff --git a/spec/frontend/ref/stores/mutations_spec.js b/spec/frontend/ref/stores/mutations_spec.js
index 78117436c33..cda13089766 100644
--- a/spec/frontend/ref/stores/mutations_spec.js
+++ b/spec/frontend/ref/stores/mutations_spec.js
@@ -1,7 +1,7 @@
-import createState from '~/ref/stores/state';
-import mutations from '~/ref/stores/mutations';
-import * as types from '~/ref/stores/mutation_types';
import { X_TOTAL_HEADER } from '~/ref/constants';
+import * as types from '~/ref/stores/mutation_types';
+import mutations from '~/ref/stores/mutations';
+import createState from '~/ref/stores/state';
describe('Ref selector Vuex store mutations', () => {
let state;
diff --git a/spec/frontend/registry/explorer/components/delete_button_spec.js b/spec/frontend/registry/explorer/components/delete_button_spec.js
index cd43e97009b..a557d9afacc 100644
--- a/spec/frontend/registry/explorer/components/delete_button_spec.js
+++ b/spec/frontend/registry/explorer/components/delete_button_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import component from '~/registry/explorer/components/delete_button.vue';
diff --git a/spec/frontend/registry/explorer/components/delete_image_spec.js b/spec/frontend/registry/explorer/components/delete_image_spec.js
new file mode 100644
index 00000000000..9a0d070e42b
--- /dev/null
+++ b/spec/frontend/registry/explorer/components/delete_image_spec.js
@@ -0,0 +1,152 @@
+import { shallowMount } from '@vue/test-utils';
+import waitForPromises from 'helpers/wait_for_promises';
+import component from '~/registry/explorer/components/delete_image.vue';
+import { GRAPHQL_PAGE_SIZE } from '~/registry/explorer/constants/index';
+import deleteContainerRepositoryMutation from '~/registry/explorer/graphql/mutations/delete_container_repository.mutation.graphql';
+import getContainerRepositoryDetailsQuery from '~/registry/explorer/graphql/queries/get_container_repository_details.query.graphql';
+
+describe('Delete Image', () => {
+ let wrapper;
+ const id = '1';
+ const storeMock = {
+ readQuery: jest.fn().mockReturnValue({
+ containerRepository: {
+ status: 'foo',
+ },
+ }),
+ writeQuery: jest.fn(),
+ };
+
+ const updatePayload = {
+ data: {
+ destroyContainerRepository: {
+ containerRepository: {
+ status: 'baz',
+ },
+ },
+ },
+ };
+
+ const findButton = () => wrapper.find('button');
+
+ const mountComponent = ({
+ propsData = { id },
+ mutate = jest.fn().mockResolvedValue({}),
+ } = {}) => {
+ wrapper = shallowMount(component, {
+ propsData,
+ mocks: {
+ $apollo: {
+ mutate,
+ },
+ },
+ scopedSlots: {
+ default: '<button @click="props.doDelete">test</button>',
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('executes apollo mutate on doDelete', () => {
+ const mutate = jest.fn().mockResolvedValue({});
+ mountComponent({ mutate });
+
+ wrapper.vm.doDelete();
+
+ expect(mutate).toHaveBeenCalledWith({
+ mutation: deleteContainerRepositoryMutation,
+ variables: {
+ id,
+ },
+ update: undefined,
+ });
+ });
+
+ it('on success emits the correct events', async () => {
+ const mutate = jest.fn().mockResolvedValue({});
+ mountComponent({ mutate });
+
+ wrapper.vm.doDelete();
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('start')).toEqual([[]]);
+ expect(wrapper.emitted('success')).toEqual([[]]);
+ expect(wrapper.emitted('end')).toEqual([[]]);
+ });
+
+ it('when a payload contains an error emits an error event', async () => {
+ const mutate = jest
+ .fn()
+ .mockResolvedValue({ data: { destroyContainerRepository: { errors: ['foo'] } } });
+
+ mountComponent({ mutate });
+ wrapper.vm.doDelete();
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[['foo']]]);
+ });
+
+ it('when the api call errors emits an error event', async () => {
+ const mutate = jest.fn().mockRejectedValue('error');
+
+ mountComponent({ mutate });
+ wrapper.vm.doDelete();
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[['error']]]);
+ });
+
+ it('uses the update function, when the prop is set to true', () => {
+ const mutate = jest.fn().mockResolvedValue({});
+
+ mountComponent({ mutate, propsData: { id, useUpdateFn: true } });
+ wrapper.vm.doDelete();
+
+ expect(mutate).toHaveBeenCalledWith({
+ mutation: deleteContainerRepositoryMutation,
+ variables: {
+ id,
+ },
+ update: wrapper.vm.updateImageStatus,
+ });
+ });
+
+ it('updateImage status reads and write to the cache', () => {
+ mountComponent();
+
+ const variables = {
+ id,
+ first: GRAPHQL_PAGE_SIZE,
+ };
+
+ wrapper.vm.updateImageStatus(storeMock, updatePayload);
+
+ expect(storeMock.readQuery).toHaveBeenCalledWith({
+ query: getContainerRepositoryDetailsQuery,
+ variables,
+ });
+ expect(storeMock.writeQuery).toHaveBeenCalledWith({
+ query: getContainerRepositoryDetailsQuery,
+ variables,
+ data: {
+ containerRepository: {
+ status: updatePayload.data.destroyContainerRepository.containerRepository.status,
+ },
+ },
+ });
+ });
+
+ it('binds the doDelete function to the default scoped slot', () => {
+ const mutate = jest.fn().mockResolvedValue({});
+ mountComponent({ mutate });
+ findButton().trigger('click');
+ expect(mutate).toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/registry/explorer/components/details_page/delete_alert_spec.js b/spec/frontend/registry/explorer/components/details_page/delete_alert_spec.js
index 6a7fbbe367a..c2a2a4e06ea 100644
--- a/spec/frontend/registry/explorer/components/details_page/delete_alert_spec.js
+++ b/spec/frontend/registry/explorer/components/details_page/delete_alert_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import component from '~/registry/explorer/components/details_page/delete_alert.vue';
import {
DELETE_TAG_SUCCESS_MESSAGE,
diff --git a/spec/frontend/registry/explorer/components/details_page/delete_modal_spec.js b/spec/frontend/registry/explorer/components/details_page/delete_modal_spec.js
index 636e0a285a6..8fe659694ba 100644
--- a/spec/frontend/registry/explorer/components/details_page/delete_modal_spec.js
+++ b/spec/frontend/registry/explorer/components/details_page/delete_modal_spec.js
@@ -1,9 +1,11 @@
-import { shallowMount } from '@vue/test-utils';
import { GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import component from '~/registry/explorer/components/details_page/delete_modal.vue';
import {
REMOVE_TAG_CONFIRMATION_TEXT,
REMOVE_TAGS_CONFIRMATION_TEXT,
+ DELETE_IMAGE_CONFIRMATION_TITLE,
+ DELETE_IMAGE_CONFIRMATION_TEXT,
} from '~/registry/explorer/constants';
import { GlModal } from '../../stubs';
@@ -35,13 +37,13 @@ describe('Delete Modal', () => {
describe('events', () => {
it.each`
- glEvent | localEvent
- ${'ok'} | ${'confirmDelete'}
- ${'cancel'} | ${'cancelDelete'}
+ glEvent | localEvent
+ ${'primary'} | ${'confirmDelete'}
+ ${'cancel'} | ${'cancelDelete'}
`('GlModal $glEvent emits $localEvent', ({ glEvent, localEvent }) => {
mountComponent();
findModal().vm.$emit(glEvent);
- expect(wrapper.emitted(localEvent)).toBeTruthy();
+ expect(wrapper.emitted(localEvent)).toEqual([[]]);
});
});
@@ -53,27 +55,51 @@ describe('Delete Modal', () => {
});
});
- describe('itemsToBeDeleted contains one element', () => {
- beforeEach(() => {
- mountComponent({ itemsToBeDeleted: [{ path: 'foo' }] });
- });
- it(`has the correct description`, () => {
- expect(findDescription().text()).toBe(REMOVE_TAG_CONFIRMATION_TEXT.replace('%{item}', 'foo'));
+ describe('when we are deleting images', () => {
+ it('has the correct title', () => {
+ mountComponent({ deleteImage: true });
+
+ expect(wrapper.text()).toContain(DELETE_IMAGE_CONFIRMATION_TITLE);
});
- it('has the correct action', () => {
- expect(wrapper.text()).toContain('Remove tag');
+
+ it('has the correct description', () => {
+ mountComponent({ deleteImage: true });
+
+ expect(wrapper.text()).toContain(DELETE_IMAGE_CONFIRMATION_TEXT);
});
});
- describe('itemsToBeDeleted contains more than element', () => {
- beforeEach(() => {
- mountComponent({ itemsToBeDeleted: [{ path: 'foo' }, { path: 'bar' }] });
- });
- it(`has the correct description`, () => {
- expect(findDescription().text()).toBe(REMOVE_TAGS_CONFIRMATION_TEXT.replace('%{item}', '2'));
+ describe('when we are deleting tags', () => {
+ describe('itemsToBeDeleted contains one element', () => {
+ beforeEach(() => {
+ mountComponent({ itemsToBeDeleted: [{ path: 'foo' }] });
+ });
+
+ it(`has the correct description`, () => {
+ expect(findDescription().text()).toBe(
+ REMOVE_TAG_CONFIRMATION_TEXT.replace('%{item}', 'foo'),
+ );
+ });
+
+ it('has the correct title', () => {
+ expect(wrapper.text()).toContain('Remove tag');
+ });
});
- it('has the correct action', () => {
- expect(wrapper.text()).toContain('Remove tags');
+
+ describe('itemsToBeDeleted contains more than element', () => {
+ beforeEach(() => {
+ mountComponent({ itemsToBeDeleted: [{ path: 'foo' }, { path: 'bar' }] });
+ });
+
+ it(`has the correct description`, () => {
+ expect(findDescription().text()).toBe(
+ REMOVE_TAGS_CONFIRMATION_TEXT.replace('%{item}', '2'),
+ );
+ });
+
+ it('has the correct title', () => {
+ expect(wrapper.text()).toContain('Remove tags');
+ });
});
});
});
diff --git a/spec/frontend/registry/explorer/components/details_page/details_header_spec.js b/spec/frontend/registry/explorer/components/details_page/details_header_spec.js
index 337235e3de5..3fa3a2ae1de 100644
--- a/spec/frontend/registry/explorer/components/details_page/details_header_spec.js
+++ b/spec/frontend/registry/explorer/components/details_page/details_header_spec.js
@@ -1,7 +1,6 @@
+import { GlSprintf, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { GlSprintf } from '@gitlab/ui';
import { useFakeDate } from 'helpers/fake_date';
-import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import component from '~/registry/explorer/components/details_page/details_header.vue';
import {
DETAILS_PAGE_TITLE,
@@ -15,6 +14,7 @@ import {
CLEANUP_ONGOING_TOOLTIP,
CLEANUP_UNFINISHED_TOOLTIP,
} from '~/registry/explorer/constants';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
describe('Details Header', () => {
let wrapper;
@@ -23,6 +23,7 @@ describe('Details Header', () => {
name: 'foo',
updatedAt: '2020-11-03T13:29:21Z',
tagsCount: 10,
+ canDelete: true,
project: {
visibility: 'public',
containerExpirationPolicy: {
@@ -36,8 +37,10 @@ describe('Details Header', () => {
const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`);
const findLastUpdatedAndVisibility = () => findByTestId('updated-and-visibility');
+ const findTitle = () => findByTestId('title');
const findTagsCount = () => findByTestId('tags-count');
const findCleanup = () => findByTestId('cleanup');
+ const findDeleteButton = () => wrapper.find(GlButton);
const waitForMetadataItems = async () => {
// Metadata items are printed by a loop in the title-area and it takes two ticks for them to be available
@@ -45,11 +48,9 @@ describe('Details Header', () => {
await wrapper.vm.$nextTick();
};
- const mountComponent = (image = defaultImage) => {
+ const mountComponent = (propsData = { image: defaultImage }) => {
wrapper = shallowMount(component, {
- propsData: {
- image,
- },
+ propsData,
stubs: {
GlSprintf,
TitleArea,
@@ -63,13 +64,65 @@ describe('Details Header', () => {
});
it('has the correct title ', () => {
- mountComponent({ ...defaultImage, name: '' });
- expect(wrapper.text()).toMatchInterpolatedText(DETAILS_PAGE_TITLE);
+ mountComponent({ image: { ...defaultImage, name: '' } });
+ expect(findTitle().text()).toMatchInterpolatedText(DETAILS_PAGE_TITLE);
});
it('shows imageName in the title', () => {
mountComponent();
- expect(wrapper.text()).toContain('foo');
+ expect(findTitle().text()).toContain('foo');
+ });
+
+ describe('delete button', () => {
+ it('exists', () => {
+ mountComponent();
+
+ expect(findDeleteButton().exists()).toBe(true);
+ });
+
+ it('is hidden while loading', () => {
+ mountComponent({ image: defaultImage, metadataLoading: true });
+
+ expect(findDeleteButton().exists()).toBe(false);
+ });
+
+ it('has the correct text', () => {
+ mountComponent();
+
+ expect(findDeleteButton().text()).toBe('Delete');
+ });
+
+ it('has the correct props', () => {
+ mountComponent();
+
+ expect(findDeleteButton().props()).toMatchObject({
+ variant: 'danger',
+ disabled: false,
+ });
+ });
+
+ it('emits the correct event', () => {
+ mountComponent();
+
+ findDeleteButton().vm.$emit('click');
+
+ expect(wrapper.emitted('delete')).toEqual([[]]);
+ });
+
+ it.each`
+ canDelete | disabled | isDisabled
+ ${true} | ${false} | ${false}
+ ${true} | ${true} | ${true}
+ ${false} | ${false} | ${true}
+ ${false} | ${true} | ${true}
+ `(
+ 'when canDelete is $canDelete and disabled is $disabled is $isDisabled that the button is disabled',
+ ({ canDelete, disabled, isDisabled }) => {
+ mountComponent({ image: { ...defaultImage, canDelete }, disabled });
+
+ expect(findDeleteButton().props('disabled')).toBe(isDisabled);
+ },
+ );
});
describe('metadata items', () => {
@@ -82,7 +135,7 @@ describe('Details Header', () => {
});
it('when there is one tag has the correct text', async () => {
- mountComponent({ ...defaultImage, tagsCount: 1 });
+ mountComponent({ image: { ...defaultImage, tagsCount: 1 } });
await waitForMetadataItems();
expect(findTagsCount().props('text')).toBe('1 tag');
@@ -124,10 +177,12 @@ describe('Details Header', () => {
'when the status is $status the text is $text and the tooltip is $tooltip',
async ({ status, text, tooltip }) => {
mountComponent({
- ...defaultImage,
- expirationPolicyCleanupStatus: status,
- project: {
- containerExpirationPolicy: { enabled: true, nextRunAt: '2021-01-03T14:29:21Z' },
+ image: {
+ ...defaultImage,
+ expirationPolicyCleanupStatus: status,
+ project: {
+ containerExpirationPolicy: { enabled: true, nextRunAt: '2021-01-03T14:29:21Z' },
+ },
},
});
await waitForMetadataItems();
@@ -156,7 +211,7 @@ describe('Details Header', () => {
expect(findLastUpdatedAndVisibility().props('icon')).toBe('eye');
});
it('shows an eye slashed when the project is not public', async () => {
- mountComponent({ ...defaultImage, project: { visibility: 'private' } });
+ mountComponent({ image: { ...defaultImage, project: { visibility: 'private' } } });
await waitForMetadataItems();
expect(findLastUpdatedAndVisibility().props('icon')).toBe('eye-slash');
diff --git a/spec/frontend/registry/explorer/components/details_page/empty_state_spec.js b/spec/frontend/registry/explorer/components/details_page/empty_state_spec.js
new file mode 100644
index 00000000000..14b15945631
--- /dev/null
+++ b/spec/frontend/registry/explorer/components/details_page/empty_state_spec.js
@@ -0,0 +1,54 @@
+import { GlEmptyState } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import component from '~/registry/explorer/components/details_page/empty_state.vue';
+import {
+ NO_TAGS_TITLE,
+ NO_TAGS_MESSAGE,
+ MISSING_OR_DELETED_IMAGE_TITLE,
+ MISSING_OR_DELETED_IMAGE_MESSAGE,
+} from '~/registry/explorer/constants';
+
+describe('EmptyTagsState component', () => {
+ let wrapper;
+
+ const findEmptyState = () => wrapper.find(GlEmptyState);
+
+ const mountComponent = (propsData) => {
+ wrapper = shallowMount(component, {
+ stubs: {
+ GlEmptyState,
+ },
+ propsData,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('contains gl-empty-state', () => {
+ mountComponent();
+ expect(findEmptyState().exists()).toBe(true);
+ });
+
+ it.each`
+ isEmptyImage | title | description
+ ${false} | ${NO_TAGS_TITLE} | ${NO_TAGS_MESSAGE}
+ ${true} | ${MISSING_OR_DELETED_IMAGE_TITLE} | ${MISSING_OR_DELETED_IMAGE_MESSAGE}
+ `(
+ 'when isEmptyImage is $isEmptyImage has the correct props',
+ ({ isEmptyImage, title, description }) => {
+ mountComponent({
+ noContainersImage: 'foo',
+ isEmptyImage,
+ });
+
+ expect(findEmptyState().props()).toMatchObject({
+ title,
+ description,
+ svgPath: 'foo',
+ });
+ },
+ );
+});
diff --git a/spec/frontend/registry/explorer/components/details_page/empty_tags_state_spec.js b/spec/frontend/registry/explorer/components/details_page/empty_tags_state_spec.js
deleted file mode 100644
index 09afd9d2d84..00000000000
--- a/spec/frontend/registry/explorer/components/details_page/empty_tags_state_spec.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlEmptyState } from '@gitlab/ui';
-import component from '~/registry/explorer/components/details_page/empty_tags_state.vue';
-import {
- EMPTY_IMAGE_REPOSITORY_TITLE,
- EMPTY_IMAGE_REPOSITORY_MESSAGE,
-} from '~/registry/explorer/constants';
-
-describe('EmptyTagsState component', () => {
- let wrapper;
-
- const findEmptyState = () => wrapper.find(GlEmptyState);
-
- const mountComponent = () => {
- wrapper = shallowMount(component, {
- stubs: {
- GlEmptyState,
- },
- propsData: {
- noContainersImage: 'foo',
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- it('contains gl-empty-state', () => {
- mountComponent();
- expect(findEmptyState().exists()).toBe(true);
- });
-
- it('has the correct props', () => {
- mountComponent();
- expect(findEmptyState().props()).toMatchObject({
- title: EMPTY_IMAGE_REPOSITORY_TITLE,
- description: EMPTY_IMAGE_REPOSITORY_MESSAGE,
- svgPath: 'foo',
- });
- });
-});
diff --git a/spec/frontend/registry/explorer/components/details_page/partial_cleanup_alert_spec.js b/spec/frontend/registry/explorer/components/details_page/partial_cleanup_alert_spec.js
index 17821d8be31..af8a23e412c 100644
--- a/spec/frontend/registry/explorer/components/details_page/partial_cleanup_alert_spec.js
+++ b/spec/frontend/registry/explorer/components/details_page/partial_cleanup_alert_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlAlert, GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import component from '~/registry/explorer/components/details_page/partial_cleanup_alert.vue';
import { DELETE_ALERT_TITLE, DELETE_ALERT_LINK_TEXT } from '~/registry/explorer/constants';
diff --git a/spec/frontend/registry/explorer/components/details_page/status_alert_spec.js b/spec/frontend/registry/explorer/components/details_page/status_alert_spec.js
new file mode 100644
index 00000000000..b079883cefd
--- /dev/null
+++ b/spec/frontend/registry/explorer/components/details_page/status_alert_spec.js
@@ -0,0 +1,57 @@
+import { GlLink, GlSprintf, GlAlert } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import component from '~/registry/explorer/components/details_page/status_alert.vue';
+import {
+ DELETE_SCHEDULED,
+ DELETE_FAILED,
+ PACKAGE_DELETE_HELP_PAGE_PATH,
+ SCHEDULED_FOR_DELETION_STATUS_TITLE,
+ SCHEDULED_FOR_DELETION_STATUS_MESSAGE,
+ FAILED_DELETION_STATUS_TITLE,
+ FAILED_DELETION_STATUS_MESSAGE,
+} from '~/registry/explorer/constants';
+
+describe('Status Alert', () => {
+ let wrapper;
+
+ const findLink = () => wrapper.find(GlLink);
+ const findAlert = () => wrapper.find(GlAlert);
+ const findMessage = () => wrapper.find('[data-testid="message"]');
+
+ const mountComponent = (propsData) => {
+ wrapper = shallowMount(component, {
+ propsData,
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it.each`
+ status | title | variant | message | link
+ ${DELETE_SCHEDULED} | ${SCHEDULED_FOR_DELETION_STATUS_TITLE} | ${'info'} | ${SCHEDULED_FOR_DELETION_STATUS_MESSAGE} | ${PACKAGE_DELETE_HELP_PAGE_PATH}
+ ${DELETE_FAILED} | ${FAILED_DELETION_STATUS_TITLE} | ${'warning'} | ${FAILED_DELETION_STATUS_MESSAGE} | ${''}
+ `(
+ `when the status is $status, title is $title, variant is $variant, message is $message and the link is $link`,
+ ({ status, title, variant, message, link }) => {
+ mountComponent({ status });
+
+ expect(findMessage().text()).toMatchInterpolatedText(message);
+ expect(findAlert().props()).toMatchObject({
+ title,
+ variant,
+ });
+ if (link) {
+ expect(findLink().attributes()).toMatchObject({
+ target: '_blank',
+ href: link,
+ });
+ }
+ },
+ );
+});
diff --git a/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js b/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js
index c2efc71c159..8b70f84c1bd 100644
--- a/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js
+++ b/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js
@@ -1,12 +1,9 @@
-import { shallowMount } from '@vue/test-utils';
import { GlFormCheckbox, GlSprintf, GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import component from '~/registry/explorer/components/details_page/tags_list_row.vue';
import DeleteButton from '~/registry/explorer/components/delete_button.vue';
-import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
+import component from '~/registry/explorer/components/details_page/tags_list_row.vue';
import {
REMOVE_TAG_BUTTON_TITLE,
REMOVE_TAG_BUTTON_DISABLE_TOOLTIP,
@@ -14,6 +11,9 @@ import {
NOT_AVAILABLE_TEXT,
NOT_AVAILABLE_SIZE,
} from '~/registry/explorer/constants/index';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { tagsMock } from '../../mock_data';
import { ListItem } from '../../stubs';
diff --git a/spec/frontend/registry/explorer/components/details_page/tags_list_spec.js b/spec/frontend/registry/explorer/components/details_page/tags_list_spec.js
index 413795a7a57..dc6760a17bd 100644
--- a/spec/frontend/registry/explorer/components/details_page/tags_list_spec.js
+++ b/spec/frontend/registry/explorer/components/details_page/tags_list_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import component from '~/registry/explorer/components/details_page/tags_list.vue';
import TagsListRow from '~/registry/explorer/components/details_page/tags_list_row.vue';
import { TAGS_LIST_TITLE, REMOVE_TAGS_BUTTON_TITLE } from '~/registry/explorer/constants/index';
@@ -70,18 +70,25 @@ describe('Tags List', () => {
});
});
- it('is disabled when no item is selected', () => {
- mountComponent();
+ it.each`
+ disabled | doSelect | buttonDisabled
+ ${true} | ${false} | ${'true'}
+ ${true} | ${true} | ${'true'}
+ ${false} | ${false} | ${'true'}
+ ${false} | ${true} | ${undefined}
+ `(
+ 'is $buttonDisabled that the button is disabled when the component disabled state is $disabled and is $doSelect that the user selected a tag',
+ async ({ disabled, buttonDisabled, doSelect }) => {
+ mountComponent({ tags, disabled, isMobile: false });
- expect(findDeleteButton().attributes('disabled')).toBe('true');
- });
+ if (doSelect) {
+ findTagsListRow().at(0).vm.$emit('select');
+ await wrapper.vm.$nextTick();
+ }
- it('is enabled when at least one item is selected', async () => {
- mountComponent();
- findTagsListRow().at(0).vm.$emit('select');
- await wrapper.vm.$nextTick();
- expect(findDeleteButton().attributes('disabled')).toBe(undefined);
- });
+ expect(findDeleteButton().attributes('disabled')).toBe(buttonDisabled);
+ },
+ );
it('click event emits a deleted event with selected items', () => {
mountComponent();
@@ -100,12 +107,13 @@ describe('Tags List', () => {
});
it('the correct props are bound to it', () => {
- mountComponent();
+ mountComponent({ tags, disabled: true });
const rows = findTagsListRow();
expect(rows.at(0).attributes()).toMatchObject({
first: 'true',
+ disabled: 'true',
});
});
diff --git a/spec/frontend/registry/explorer/components/list_page/cli_commands_spec.js b/spec/frontend/registry/explorer/components/list_page/cli_commands_spec.js
index 74b9ea5fd96..8ca8fca65ed 100644
--- a/spec/frontend/registry/explorer/components/list_page/cli_commands_spec.js
+++ b/spec/frontend/registry/explorer/components/list_page/cli_commands_spec.js
@@ -1,9 +1,7 @@
-import Vuex from 'vuex';
-import { mount, createLocalVue } from '@vue/test-utils';
import { GlDropdown } from '@gitlab/ui';
-import Tracking from '~/tracking';
+import { mount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
import QuickstartDropdown from '~/registry/explorer/components/list_page/cli_commands.vue';
-import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
import {
QUICK_START,
LOGIN_COMMAND_LABEL,
@@ -13,6 +11,8 @@ import {
PUSH_COMMAND_LABEL,
COPY_PUSH_TITLE,
} from '~/registry/explorer/constants';
+import Tracking from '~/tracking';
+import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
import { dockerCommands } from '../../mock_data';
diff --git a/spec/frontend/registry/explorer/components/list_page/group_empty_state_spec.js b/spec/frontend/registry/explorer/components/list_page/group_empty_state_spec.js
index 1ba2036dc34..989a60625e2 100644
--- a/spec/frontend/registry/explorer/components/list_page/group_empty_state_spec.js
+++ b/spec/frontend/registry/explorer/components/list_page/group_empty_state_spec.js
@@ -1,8 +1,8 @@
-import Vuex from 'vuex';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlSprintf } from '@gitlab/ui';
-import { GlEmptyState } from '../../stubs';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
import groupEmptyState from '~/registry/explorer/components/list_page/group_empty_state.vue';
+import { GlEmptyState } from '../../stubs';
const localVue = createLocalVue();
localVue.use(Vuex);
diff --git a/spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js b/spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js
index a06c4795b2e..d6ee871341b 100644
--- a/spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js
+++ b/spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js
@@ -1,11 +1,9 @@
-import { shallowMount } from '@vue/test-utils';
import { GlIcon, GlSprintf, GlSkeletonLoader } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import Component from '~/registry/explorer/components/list_page/image_list_row.vue';
-import ListItem from '~/vue_shared/components/registry/list_item.vue';
import DeleteButton from '~/registry/explorer/components/delete_button.vue';
+import Component from '~/registry/explorer/components/list_page/image_list_row.vue';
import {
ROW_SCHEDULED_FOR_DELETION,
LIST_DELETE_BUTTON_DISABLED,
@@ -15,8 +13,10 @@ import {
IMAGE_DELETE_SCHEDULED_STATUS,
IMAGE_FAILED_DELETED_STATUS,
} from '~/registry/explorer/constants';
-import { RouterLink } from '../../stubs';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import ListItem from '~/vue_shared/components/registry/list_item.vue';
import { imagesListResponse } from '../../mock_data';
+import { RouterLink } from '../../stubs';
describe('Image List Row', () => {
let wrapper;
diff --git a/spec/frontend/registry/explorer/components/list_page/image_list_spec.js b/spec/frontend/registry/explorer/components/list_page/image_list_spec.js
index 61c362f4d78..d7dd825ca3e 100644
--- a/spec/frontend/registry/explorer/components/list_page/image_list_spec.js
+++ b/spec/frontend/registry/explorer/components/list_page/image_list_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlKeysetPagination } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import Component from '~/registry/explorer/components/list_page/image_list.vue';
import ImageListRow from '~/registry/explorer/components/list_page/image_list_row.vue';
diff --git a/spec/frontend/registry/explorer/components/list_page/project_empty_state_spec.js b/spec/frontend/registry/explorer/components/list_page/project_empty_state_spec.js
index 3a27cf1923c..111aa45f231 100644
--- a/spec/frontend/registry/explorer/components/list_page/project_empty_state_spec.js
+++ b/spec/frontend/registry/explorer/components/list_page/project_empty_state_spec.js
@@ -1,9 +1,9 @@
-import Vuex from 'vuex';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlSprintf } from '@gitlab/ui';
-import { GlEmptyState } from '../../stubs';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
import projectEmptyState from '~/registry/explorer/components/list_page/project_empty_state.vue';
import { dockerCommands } from '../../mock_data';
+import { GlEmptyState } from '../../stubs';
const localVue = createLocalVue();
localVue.use(Vuex);
diff --git a/spec/frontend/registry/explorer/components/list_page/registry_header_spec.js b/spec/frontend/registry/explorer/components/list_page/registry_header_spec.js
index 58439c185e3..07256d2bbf5 100644
--- a/spec/frontend/registry/explorer/components/list_page/registry_header_spec.js
+++ b/spec/frontend/registry/explorer/components/list_page/registry_header_spec.js
@@ -1,13 +1,13 @@
-import { shallowMount } from '@vue/test-utils';
import { GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import Component from '~/registry/explorer/components/list_page/registry_header.vue';
-import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import {
CONTAINER_REGISTRY_TITLE,
LIST_INTRO_TEXT,
EXPIRATION_POLICY_DISABLED_MESSAGE,
EXPIRATION_POLICY_DISABLED_TEXT,
} from '~/registry/explorer/constants';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
jest.mock('~/lib/utils/datetime_utility', () => ({
approximateDuration: jest.fn(),
diff --git a/spec/frontend/registry/explorer/mock_data.js b/spec/frontend/registry/explorer/mock_data.js
index b0fc009872c..f4453912db4 100644
--- a/spec/frontend/registry/explorer/mock_data.js
+++ b/spec/frontend/registry/explorer/mock_data.js
@@ -235,3 +235,9 @@ export const graphQLProjectImageRepositoriesDetailsMock = {
},
},
};
+
+export const graphQLEmptyImageDetailsMock = {
+ data: {
+ containerRepository: null,
+ },
+};
diff --git a/spec/frontend/registry/explorer/pages/details_spec.js b/spec/frontend/registry/explorer/pages/details_spec.js
index 1746a6a63b6..65c58bf9874 100644
--- a/spec/frontend/registry/explorer/pages/details_spec.js
+++ b/spec/frontend/registry/explorer/pages/details_spec.js
@@ -1,27 +1,35 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlKeysetPagination } from '@gitlab/ui';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import Tracking from '~/tracking';
-import component from '~/registry/explorer/pages/details.vue';
+import axios from '~/lib/utils/axios_utils';
+import DeleteImage from '~/registry/explorer/components/delete_image.vue';
import DeleteAlert from '~/registry/explorer/components/details_page/delete_alert.vue';
-import PartialCleanupAlert from '~/registry/explorer/components/details_page/partial_cleanup_alert.vue';
import DetailsHeader from '~/registry/explorer/components/details_page/details_header.vue';
-import TagsLoader from '~/registry/explorer/components/details_page/tags_loader.vue';
+import EmptyTagsState from '~/registry/explorer/components/details_page/empty_state.vue';
+import PartialCleanupAlert from '~/registry/explorer/components/details_page/partial_cleanup_alert.vue';
+import StatusAlert from '~/registry/explorer/components/details_page/status_alert.vue';
import TagsList from '~/registry/explorer/components/details_page/tags_list.vue';
-import EmptyTagsState from '~/registry/explorer/components/details_page/empty_tags_state.vue';
+import TagsLoader from '~/registry/explorer/components/details_page/tags_loader.vue';
-import getContainerRepositoryDetailsQuery from '~/registry/explorer/graphql/queries/get_container_repository_details.query.graphql';
+import {
+ UNFINISHED_STATUS,
+ DELETE_SCHEDULED,
+ ALERT_DANGER_IMAGE,
+} from '~/registry/explorer/constants';
import deleteContainerRepositoryTagsMutation from '~/registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql';
+import getContainerRepositoryDetailsQuery from '~/registry/explorer/graphql/queries/get_container_repository_details.query.graphql';
-import { UNFINISHED_STATUS } from '~/registry/explorer/constants/index';
+import component from '~/registry/explorer/pages/details.vue';
+import Tracking from '~/tracking';
import {
graphQLImageDetailsMock,
graphQLImageDetailsEmptyTagsMock,
graphQLDeleteImageRepositoryTagsMock,
containerRepositoryMock,
+ graphQLEmptyImageDetailsMock,
tagsMock,
tagsPageInfo,
} from '../mock_data';
@@ -39,8 +47,10 @@ describe('Details Page', () => {
const findTagsList = () => wrapper.find(TagsList);
const findDeleteAlert = () => wrapper.find(DeleteAlert);
const findDetailsHeader = () => wrapper.find(DetailsHeader);
- const findEmptyTagsState = () => wrapper.find(EmptyTagsState);
+ const findEmptyState = () => wrapper.find(EmptyTagsState);
const findPartialCleanupAlert = () => wrapper.find(PartialCleanupAlert);
+ const findStatusAlert = () => wrapper.find(StatusAlert);
+ const findDeleteImage = () => wrapper.find(DeleteImage);
const routeId = 1;
@@ -86,6 +96,7 @@ describe('Details Page', () => {
apolloProvider,
stubs: {
DeleteModal,
+ DeleteImage,
},
mocks: {
$route: {
@@ -133,6 +144,27 @@ describe('Details Page', () => {
});
});
+ describe('when the image does not exist', () => {
+ it('does not show the default ui', async () => {
+ mountComponent({ resolver: jest.fn().mockResolvedValue(graphQLEmptyImageDetailsMock) });
+
+ await waitForApolloRequestRender();
+
+ expect(findTagsLoader().exists()).toBe(false);
+ expect(findDetailsHeader().exists()).toBe(false);
+ expect(findTagsList().exists()).toBe(false);
+ expect(findPagination().exists()).toBe(false);
+ });
+
+ it('shows an empty state message', async () => {
+ mountComponent({ resolver: jest.fn().mockResolvedValue(graphQLEmptyImageDetailsMock) });
+
+ await waitForApolloRequestRender();
+
+ expect(findEmptyState().exists()).toBe(true);
+ });
+ });
+
describe('when the list of tags is empty', () => {
const resolver = jest.fn().mockResolvedValue(graphQLImageDetailsEmptyTagsMock);
@@ -141,7 +173,7 @@ describe('Details Page', () => {
await waitForApolloRequestRender();
- expect(findEmptyTagsState().exists()).toBe(true);
+ expect(findEmptyState().exists()).toBe(true);
});
it('does not show the loader', async () => {
@@ -401,6 +433,9 @@ describe('Details Page', () => {
const config = {
runCleanupPoliciesHelpPagePath: 'foo',
cleanupPoliciesHelpPagePath: 'bar',
+ userCalloutsPath: 'call_out_path',
+ userCalloutId: 'call_out_id',
+ showUnfinishedTagCleanupCallout: true,
};
describe(`when expirationPolicyCleanupStatus is ${UNFINISHED_STATUS}`, () => {
@@ -413,8 +448,9 @@ describe('Details Page', () => {
}),
);
});
+
it('exists', async () => {
- mountComponent({ resolver });
+ mountComponent({ resolver, config });
await waitForApolloRequestRender();
@@ -426,11 +462,16 @@ describe('Details Page', () => {
await waitForApolloRequestRender();
- expect(findPartialCleanupAlert().props()).toEqual({ ...config });
+ expect(findPartialCleanupAlert().props()).toEqual({
+ runCleanupPoliciesHelpPagePath: config.runCleanupPoliciesHelpPagePath,
+ cleanupPoliciesHelpPagePath: config.cleanupPoliciesHelpPagePath,
+ });
});
it('dismiss hides the component', async () => {
- mountComponent({ resolver });
+ jest.spyOn(axios, 'post').mockReturnValue();
+
+ mountComponent({ resolver, config });
await waitForApolloRequestRender();
@@ -440,13 +481,25 @@ describe('Details Page', () => {
await wrapper.vm.$nextTick();
+ expect(axios.post).toHaveBeenCalledWith(config.userCalloutsPath, {
+ feature_name: config.userCalloutId,
+ });
+ expect(findPartialCleanupAlert().exists()).toBe(false);
+ });
+
+ it('is hidden if the callout is dismissed', async () => {
+ mountComponent({ resolver });
+
+ await waitForApolloRequestRender();
+
expect(findPartialCleanupAlert().exists()).toBe(false);
});
});
describe(`when expirationPolicyCleanupStatus is not ${UNFINISHED_STATUS}`, () => {
it('the component is hidden', async () => {
- mountComponent();
+ mountComponent({ config });
+
await waitForApolloRequestRender();
expect(findPartialCleanupAlert().exists()).toBe(false);
@@ -463,4 +516,83 @@ describe('Details Page', () => {
expect(breadCrumbState.updateName).toHaveBeenCalledWith(containerRepositoryMock.name);
});
});
+
+ describe('when the image has a status different from null', () => {
+ const resolver = jest
+ .fn()
+ .mockResolvedValue(graphQLImageDetailsMock({ status: DELETE_SCHEDULED }));
+ it('disables all the actions', async () => {
+ mountComponent({ resolver });
+
+ await waitForApolloRequestRender();
+
+ expect(findDetailsHeader().props('disabled')).toBe(true);
+ expect(findTagsList().props('disabled')).toBe(true);
+ });
+
+ it('shows a status alert', async () => {
+ mountComponent({ resolver });
+
+ await waitForApolloRequestRender();
+
+ expect(findStatusAlert().exists()).toBe(true);
+ expect(findStatusAlert().props()).toMatchObject({
+ status: DELETE_SCHEDULED,
+ });
+ });
+ });
+
+ describe('delete the image', () => {
+ const mountComponentAndDeleteImage = async () => {
+ mountComponent();
+
+ await waitForApolloRequestRender();
+ findDetailsHeader().vm.$emit('delete');
+
+ await wrapper.vm.$nextTick();
+ };
+
+ it('on delete event it deletes the image', async () => {
+ await mountComponentAndDeleteImage();
+
+ findDeleteModal().vm.$emit('confirmDelete');
+
+ expect(findDeleteImage().emitted('start')).toEqual([[]]);
+ });
+
+ it('binds the correct props to the modal', async () => {
+ await mountComponentAndDeleteImage();
+
+ expect(findDeleteModal().props()).toMatchObject({
+ itemsToBeDeleted: [{ path: 'gitlab-org/gitlab-test/rails-12009' }],
+ deleteImage: true,
+ });
+ });
+
+ it('binds correctly to delete-image start and end events', async () => {
+ mountComponent();
+
+ findDeleteImage().vm.$emit('start');
+
+ await wrapper.vm.$nextTick();
+
+ expect(findTagsLoader().exists()).toBe(true);
+
+ findDeleteImage().vm.$emit('end');
+
+ await wrapper.vm.$nextTick();
+
+ expect(findTagsLoader().exists()).toBe(false);
+ });
+
+ it('binds correctly to delete-image error event', async () => {
+ mountComponent();
+
+ findDeleteImage().vm.$emit('error');
+
+ await wrapper.vm.$nextTick();
+
+ expect(findDeleteAlert().props('deleteAlertType')).toBe(ALERT_DANGER_IMAGE);
+ });
+ });
});
diff --git a/spec/frontend/registry/explorer/pages/list_spec.js b/spec/frontend/registry/explorer/pages/list_spec.js
index c4556934934..f7f207cc183 100644
--- a/spec/frontend/registry/explorer/pages/list_spec.js
+++ b/spec/frontend/registry/explorer/pages/list_spec.js
@@ -1,33 +1,32 @@
+import { GlSkeletonLoader, GlSprintf, GlAlert } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
-import { GlSkeletonLoader, GlSprintf, GlAlert, GlSearchBoxByClick } from '@gitlab/ui';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql';
-import Tracking from '~/tracking';
-import component from '~/registry/explorer/pages/list.vue';
+import DeleteImage from '~/registry/explorer/components/delete_image.vue';
import CliCommands from '~/registry/explorer/components/list_page/cli_commands.vue';
import GroupEmptyState from '~/registry/explorer/components/list_page/group_empty_state.vue';
+import ImageList from '~/registry/explorer/components/list_page/image_list.vue';
import ProjectEmptyState from '~/registry/explorer/components/list_page/project_empty_state.vue';
import RegistryHeader from '~/registry/explorer/components/list_page/registry_header.vue';
-import ImageList from '~/registry/explorer/components/list_page/image_list.vue';
-import TitleArea from '~/vue_shared/components/registry/title_area.vue';
-
import {
DELETE_IMAGE_SUCCESS_MESSAGE,
DELETE_IMAGE_ERROR_MESSAGE,
- IMAGE_REPOSITORY_LIST_LABEL,
- SEARCH_PLACEHOLDER_TEXT,
+ SORT_FIELDS,
} from '~/registry/explorer/constants';
-
-import getContainerRepositoriesDetails from '~/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql';
import deleteContainerRepositoryMutation from '~/registry/explorer/graphql/mutations/delete_container_repository.mutation.graphql';
+import getContainerRepositoriesDetails from '~/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql';
+import component from '~/registry/explorer/pages/list.vue';
+import Tracking from '~/tracking';
+import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
+import { $toast } from '../../shared/mocks';
import {
graphQLImageListMock,
graphQLImageDeleteMock,
deletedContainerRepository,
- graphQLImageDeleteMockError,
graphQLEmptyImageListMock,
graphQLEmptyGroupImageListMock,
pageInfo,
@@ -35,7 +34,6 @@ import {
dockerCommands,
} from '../mock_data';
import { GlModal, GlEmptyState } from '../stubs';
-import { $toast } from '../../shared/mocks';
const localVue = createLocalVue();
@@ -55,9 +53,9 @@ describe('List Page', () => {
const findDeleteAlert = () => wrapper.find(GlAlert);
const findImageList = () => wrapper.find(ImageList);
- const findListHeader = () => wrapper.find('[data-testid="listHeader"]');
- const findSearchBox = () => wrapper.find(GlSearchBoxByClick);
+ const findRegistrySearch = () => wrapper.find(RegistrySearch);
const findEmptySearchMessage = () => wrapper.find('[data-testid="emptySearch"]');
+ const findDeleteImage = () => wrapper.find(DeleteImage);
const waitForApolloRequestRender = async () => {
jest.runOnlyPendingTimers();
@@ -91,6 +89,7 @@ describe('List Page', () => {
GlSprintf,
RegistryHeader,
TitleArea,
+ DeleteImage,
},
mocks: {
$toast,
@@ -227,14 +226,6 @@ describe('List Page', () => {
expect(findCliCommands().exists()).toBe(false);
});
-
- it('list header is not visible', async () => {
- mountComponent({ resolver, config });
-
- await waitForApolloRequestRender();
-
- expect(findListHeader().exists()).toBe(false);
- });
});
});
@@ -256,16 +247,6 @@ describe('List Page', () => {
expect(findImageList().exists()).toBe(true);
});
- it('list header is visible', async () => {
- mountComponent();
-
- await waitForApolloRequestRender();
-
- const header = findListHeader();
- expect(header.exists()).toBe(true);
- expect(header.text()).toBe(IMAGE_REPOSITORY_LIST_LABEL);
- });
-
describe('additional metadata', () => {
it('is called on component load', async () => {
const detailsResolver = jest
@@ -300,23 +281,22 @@ describe('List Page', () => {
});
describe('delete image', () => {
- const deleteImage = async () => {
- await wrapper.vm.$nextTick();
+ const selectImageForDeletion = async () => {
+ await waitForApolloRequestRender();
findImageList().vm.$emit('delete', deletedContainerRepository);
- findDeleteModal().vm.$emit('ok');
-
- await waitForApolloRequestRender();
};
it('should call deleteItem when confirming deletion', async () => {
const mutationResolver = jest.fn().mockResolvedValue(graphQLImageDeleteMock);
mountComponent({ mutationResolver });
- await deleteImage();
+ await selectImageForDeletion();
+
+ findDeleteModal().vm.$emit('primary');
+ await waitForApolloRequestRender();
expect(wrapper.vm.itemToDelete).toEqual(deletedContainerRepository);
- expect(mutationResolver).toHaveBeenCalledWith({ id: deletedContainerRepository.id });
const updatedImage = findImageList()
.props('images')
@@ -326,10 +306,12 @@ describe('List Page', () => {
});
it('should show a success alert when delete request is successful', async () => {
- const mutationResolver = jest.fn().mockResolvedValue(graphQLImageDeleteMock);
- mountComponent({ mutationResolver });
+ mountComponent();
+
+ await selectImageForDeletion();
- await deleteImage();
+ findDeleteImage().vm.$emit('success');
+ await wrapper.vm.$nextTick();
const alert = findDeleteAlert();
expect(alert.exists()).toBe(true);
@@ -340,23 +322,12 @@ describe('List Page', () => {
describe('when delete request fails it shows an alert', () => {
it('user recoverable error', async () => {
- const mutationResolver = jest.fn().mockResolvedValue(graphQLImageDeleteMockError);
- mountComponent({ mutationResolver });
-
- await deleteImage();
-
- const alert = findDeleteAlert();
- expect(alert.exists()).toBe(true);
- expect(alert.text().replace(/\s\s+/gm, ' ')).toBe(
- DELETE_IMAGE_ERROR_MESSAGE.replace('%{title}', wrapper.vm.itemToDelete.path),
- );
- });
+ mountComponent();
- it('network error', async () => {
- const mutationResolver = jest.fn().mockRejectedValue();
- mountComponent({ mutationResolver });
+ await selectImageForDeletion();
- await deleteImage();
+ findDeleteImage().vm.$emit('error');
+ await wrapper.vm.$nextTick();
const alert = findDeleteAlert();
expect(alert.exists()).toBe(true);
@@ -368,10 +339,15 @@ describe('List Page', () => {
});
});
- describe('search', () => {
+ describe('search and sorting', () => {
const doSearch = async () => {
await waitForApolloRequestRender();
- findSearchBox().vm.$emit('submit', 'centos6');
+ findRegistrySearch().vm.$emit('filter:changed', [
+ { type: 'filtered-search-term', value: { data: 'centos6' } },
+ ]);
+
+ findRegistrySearch().vm.$emit('filter:submit');
+
await wrapper.vm.$nextTick();
};
@@ -380,9 +356,26 @@ describe('List Page', () => {
await waitForApolloRequestRender();
- const searchBox = findSearchBox();
- expect(searchBox.exists()).toBe(true);
- expect(searchBox.attributes('placeholder')).toBe(SEARCH_PLACEHOLDER_TEXT);
+ const registrySearch = findRegistrySearch();
+ expect(registrySearch.exists()).toBe(true);
+ expect(registrySearch.props()).toMatchObject({
+ filter: [],
+ sorting: { orderBy: 'UPDATED', sort: 'desc' },
+ sortableFields: SORT_FIELDS,
+ tokens: [],
+ });
+ });
+
+ it('performs sorting', async () => {
+ const resolver = jest.fn().mockResolvedValue(graphQLImageListMock);
+ mountComponent({ resolver });
+
+ await waitForApolloRequestRender();
+
+ findRegistrySearch().vm.$emit('sorting:changed', { sort: 'asc' });
+ await wrapper.vm.$nextTick();
+
+ expect(resolver).toHaveBeenCalledWith(expect.objectContaining({ sort: 'UPDATED_DESC' }));
});
it('performs a search', async () => {
@@ -499,9 +492,8 @@ describe('List Page', () => {
testTrackingCall('cancel_delete');
});
- it('send an event when confirm is clicked on modal', () => {
- const deleteModal = findDeleteModal();
- deleteModal.vm.$emit('ok');
+ it('send an event when the deletion starts', () => {
+ findDeleteImage().vm.$emit('start');
testTrackingCall('confirm_delete');
});
});
diff --git a/spec/frontend/registry/settings/components/expiration_input_spec.js b/spec/frontend/registry/settings/components/expiration_input_spec.js
index 383158067dc..b91599a2789 100644
--- a/spec/frontend/registry/settings/components/expiration_input_spec.js
+++ b/spec/frontend/registry/settings/components/expiration_input_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlSprintf, GlFormInput, GlLink } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { GlFormGroup } from 'jest/registry/shared/stubs';
import component from '~/registry/settings/components/expiration_input.vue';
import { NAME_REGEX_LENGTH } from '~/registry/settings/constants';
diff --git a/spec/frontend/registry/settings/components/expiration_run_text_spec.js b/spec/frontend/registry/settings/components/expiration_run_text_spec.js
index d74ee2d1c18..753bb10ad08 100644
--- a/spec/frontend/registry/settings/components/expiration_run_text_spec.js
+++ b/spec/frontend/registry/settings/components/expiration_run_text_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlFormInput } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { GlFormGroup } from 'jest/registry/shared/stubs';
import component from '~/registry/settings/components/expiration_run_text.vue';
import { NEXT_CLEANUP_LABEL, NOT_SCHEDULED_POLICY_TEXT } from '~/registry/settings/constants';
diff --git a/spec/frontend/registry/settings/components/expiration_toggle_spec.js b/spec/frontend/registry/settings/components/expiration_toggle_spec.js
index ce016e852ee..961bdfdf2c5 100644
--- a/spec/frontend/registry/settings/components/expiration_toggle_spec.js
+++ b/spec/frontend/registry/settings/components/expiration_toggle_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlToggle, GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { GlFormGroup } from 'jest/registry/shared/stubs';
import component from '~/registry/settings/components/expiration_toggle.vue';
import {
diff --git a/spec/frontend/registry/settings/components/registry_settings_app_spec.js b/spec/frontend/registry/settings/components/registry_settings_app_spec.js
index 32d4f16221d..fd53efa884f 100644
--- a/spec/frontend/registry/settings/components/registry_settings_app_spec.js
+++ b/spec/frontend/registry/settings/components/registry_settings_app_spec.js
@@ -1,15 +1,15 @@
+import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
-import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
import createMockApollo from 'helpers/mock_apollo_helper';
import component from '~/registry/settings/components/registry_settings_app.vue';
-import expirationPolicyQuery from '~/registry/settings/graphql/queries/get_expiration_policy.query.graphql';
import SettingsForm from '~/registry/settings/components/settings_form.vue';
import {
FETCH_SETTINGS_ERROR_MESSAGE,
UNAVAILABLE_FEATURE_INTRO_TEXT,
UNAVAILABLE_USER_FEATURE_TEXT,
} from '~/registry/settings/constants';
+import expirationPolicyQuery from '~/registry/settings/graphql/queries/get_expiration_policy.query.graphql';
import {
expirationPolicyPayload,
diff --git a/spec/frontend/registry/settings/components/settings_form_spec.js b/spec/frontend/registry/settings/components/settings_form_spec.js
index 626f4fcc9f5..7527910ad59 100644
--- a/spec/frontend/registry/settings/components/settings_form_spec.js
+++ b/spec/frontend/registry/settings/components/settings_form_spec.js
@@ -2,14 +2,14 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import Tracking from '~/tracking';
import component from '~/registry/settings/components/settings_form.vue';
-import updateContainerExpirationPolicyMutation from '~/registry/settings/graphql/mutations/update_container_expiration_policy.mutation.graphql';
-import expirationPolicyQuery from '~/registry/settings/graphql/queries/get_expiration_policy.query.graphql';
import {
UPDATE_SETTINGS_ERROR_MESSAGE,
UPDATE_SETTINGS_SUCCESS_MESSAGE,
} from '~/registry/settings/constants';
+import updateContainerExpirationPolicyMutation from '~/registry/settings/graphql/mutations/update_container_expiration_policy.mutation.graphql';
+import expirationPolicyQuery from '~/registry/settings/graphql/queries/get_expiration_policy.query.graphql';
+import Tracking from '~/tracking';
import { GlCard, GlLoadingIcon } from '../../shared/stubs';
import { expirationPolicyPayload, expirationPolicyMutationPayload } from '../mock_data';
diff --git a/spec/frontend/registry/settings/graphql/cache_updated_spec.js b/spec/frontend/registry/settings/graphql/cache_updated_spec.js
index d88a5576f26..73655b6917b 100644
--- a/spec/frontend/registry/settings/graphql/cache_updated_spec.js
+++ b/spec/frontend/registry/settings/graphql/cache_updated_spec.js
@@ -1,5 +1,5 @@
-import { updateContainerExpirationPolicy } from '~/registry/settings/graphql/utils/cache_update';
import expirationPolicyQuery from '~/registry/settings/graphql/queries/get_expiration_policy.query.graphql';
+import { updateContainerExpirationPolicy } from '~/registry/settings/graphql/utils/cache_update';
describe('Registry settings cache update', () => {
let client;
diff --git a/spec/frontend/related_merge_requests/components/related_merge_requests_spec.js b/spec/frontend/related_merge_requests/components/related_merge_requests_spec.js
index c084410c65b..f306fdef624 100644
--- a/spec/frontend/related_merge_requests/components/related_merge_requests_spec.js
+++ b/spec/frontend/related_merge_requests/components/related_merge_requests_spec.js
@@ -1,9 +1,9 @@
import { mount, createLocalVue } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
-import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
import RelatedMergeRequests from '~/related_merge_requests/components/related_merge_requests.vue';
import createStore from '~/related_merge_requests/store/index';
+import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
const FIXTURE_PATH = 'issues/related_merge_requests.json';
const API_ENDPOINT = '/api/v4/projects/2/issues/33/related_merge_requests';
diff --git a/spec/frontend/related_merge_requests/store/actions_spec.js b/spec/frontend/related_merge_requests/store/actions_spec.js
index a4257d67176..a14096388e6 100644
--- a/spec/frontend/related_merge_requests/store/actions_spec.js
+++ b/spec/frontend/related_merge_requests/store/actions_spec.js
@@ -2,8 +2,8 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import * as types from '~/related_merge_requests/store/mutation_types';
import * as actions from '~/related_merge_requests/store/actions';
+import * as types from '~/related_merge_requests/store/mutation_types';
jest.mock('~/flash');
diff --git a/spec/frontend/related_merge_requests/store/mutations_spec.js b/spec/frontend/related_merge_requests/store/mutations_spec.js
index 21b6e26376b..436c7dca6ce 100644
--- a/spec/frontend/related_merge_requests/store/mutations_spec.js
+++ b/spec/frontend/related_merge_requests/store/mutations_spec.js
@@ -1,5 +1,5 @@
-import mutations from '~/related_merge_requests/store/mutations';
import * as types from '~/related_merge_requests/store/mutation_types';
+import mutations from '~/related_merge_requests/store/mutations';
describe('RelatedMergeRequests Store Mutations', () => {
describe('SET_INITIAL_STATE', () => {
diff --git a/spec/frontend/releases/components/app_edit_new_spec.js b/spec/frontend/releases/components/app_edit_new_spec.js
index 828d1b46a80..1e55ab8f9e4 100644
--- a/spec/frontend/releases/components/app_edit_new_spec.js
+++ b/spec/frontend/releases/components/app_edit_new_spec.js
@@ -1,13 +1,13 @@
-import Vuex from 'vuex';
import { mount } from '@vue/test-utils';
-import { merge } from 'lodash';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
+import { merge } from 'lodash';
+import Vuex from 'vuex';
import { getJSONFixture } from 'helpers/fixtures';
-import ReleaseEditNewApp from '~/releases/components/app_edit_new.vue';
import * as commonUtils from '~/lib/utils/common_utils';
-import { BACK_URL_PARAM } from '~/releases/constants';
+import ReleaseEditNewApp from '~/releases/components/app_edit_new.vue';
import AssetLinksForm from '~/releases/components/asset_links_form.vue';
+import { BACK_URL_PARAM } from '~/releases/constants';
const originalRelease = getJSONFixture('api/releases/release.json');
const originalMilestones = originalRelease.milestones;
diff --git a/spec/frontend/releases/components/app_index_spec.js b/spec/frontend/releases/components/app_index_spec.js
index 1481dd30fd4..2b5270e29d6 100644
--- a/spec/frontend/releases/components/app_index_spec.js
+++ b/spec/frontend/releases/components/app_index_spec.js
@@ -1,15 +1,15 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
import { range as rge } from 'lodash';
import Vuex from 'vuex';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import waitForPromises from 'helpers/wait_for_promises';
import { getJSONFixture } from 'helpers/fixtures';
+import waitForPromises from 'helpers/wait_for_promises';
+import api from '~/api';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import ReleasesApp from '~/releases/components/app_index.vue';
+import ReleasesPagination from '~/releases/components/releases_pagination.vue';
import createStore from '~/releases/stores';
import createListModule from '~/releases/stores/modules/list';
-import api from '~/api';
import { pageInfoHeadersWithoutPagination, pageInfoHeadersWithPagination } from '../mock_data';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import ReleasesPagination from '~/releases/components/releases_pagination.vue';
jest.mock('~/lib/utils/common_utils', () => ({
...jest.requireActual('~/lib/utils/common_utils'),
diff --git a/spec/frontend/releases/components/app_show_spec.js b/spec/frontend/releases/components/app_show_spec.js
index b1f94ca9859..5caea395f0a 100644
--- a/spec/frontend/releases/components/app_show_spec.js
+++ b/spec/frontend/releases/components/app_show_spec.js
@@ -1,10 +1,10 @@
-import Vuex from 'vuex';
import { shallowMount } from '@vue/test-utils';
+import Vuex from 'vuex';
import { getJSONFixture } from 'helpers/fixtures';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import ReleaseShowApp from '~/releases/components/app_show.vue';
-import ReleaseSkeletonLoader from '~/releases/components/release_skeleton_loader.vue';
import ReleaseBlock from '~/releases/components/release_block.vue';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import ReleaseSkeletonLoader from '~/releases/components/release_skeleton_loader.vue';
const originalRelease = getJSONFixture('api/releases/release.json');
diff --git a/spec/frontend/releases/components/asset_links_form_spec.js b/spec/frontend/releases/components/asset_links_form_spec.js
index 2b2ad0150ab..bbaa4e9dc94 100644
--- a/spec/frontend/releases/components/asset_links_form_spec.js
+++ b/spec/frontend/releases/components/asset_links_form_spec.js
@@ -1,9 +1,9 @@
-import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
import { getJSONFixture } from 'helpers/fixtures';
-import AssetLinksForm from '~/releases/components/asset_links_form.vue';
import * as commonUtils from '~/lib/utils/common_utils';
import { ENTER_KEY } from '~/lib/utils/keys';
+import AssetLinksForm from '~/releases/components/asset_links_form.vue';
import { ASSET_LINK_TYPE, DEFAULT_ASSET_LINK_TYPE } from '~/releases/constants';
const localVue = createLocalVue();
diff --git a/spec/frontend/releases/components/evidence_block_spec.js b/spec/frontend/releases/components/evidence_block_spec.js
index b8c78f90fc2..50b6d1c4707 100644
--- a/spec/frontend/releases/components/evidence_block_spec.js
+++ b/spec/frontend/releases/components/evidence_block_spec.js
@@ -1,10 +1,10 @@
-import { mount } from '@vue/test-utils';
import { GlLink, GlIcon } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import { getJSONFixture } from 'helpers/fixtures';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { truncateSha } from '~/lib/utils/text_utility';
import EvidenceBlock from '~/releases/components/evidence_block.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
const originalRelease = getJSONFixture('api/releases/release.json');
diff --git a/spec/frontend/releases/components/release_block_assets_spec.js b/spec/frontend/releases/components/release_block_assets_spec.js
index 77bd35f94aa..3b9b16fa890 100644
--- a/spec/frontend/releases/components/release_block_assets_spec.js
+++ b/spec/frontend/releases/components/release_block_assets_spec.js
@@ -1,7 +1,7 @@
-import { mount } from '@vue/test-utils';
import { GlCollapse } from '@gitlab/ui';
-import { trimText } from 'helpers/text_helper';
+import { mount } from '@vue/test-utils';
import { getJSONFixture } from 'helpers/fixtures';
+import { trimText } from 'helpers/text_helper';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import ReleaseBlockAssets from '~/releases/components/release_block_assets.vue';
import { ASSET_LINK_TYPE } from '~/releases/constants';
diff --git a/spec/frontend/releases/components/release_block_footer_spec.js b/spec/frontend/releases/components/release_block_footer_spec.js
index f1c0c24f8ca..e9fa22b4ec7 100644
--- a/spec/frontend/releases/components/release_block_footer_spec.js
+++ b/spec/frontend/releases/components/release_block_footer_spec.js
@@ -1,26 +1,16 @@
-import { mount } from '@vue/test-utils';
import { GlLink, GlIcon } from '@gitlab/ui';
-import { trimText } from 'helpers/text_helper';
-import { getJSONFixture } from 'helpers/fixtures';
+import { mount } from '@vue/test-utils';
import { cloneDeep } from 'lodash';
-import ReleaseBlockFooter from '~/releases/components/release_block_footer.vue';
+import { getJSONFixture } from 'helpers/fixtures';
+import { trimText } from 'helpers/text_helper';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import ReleaseBlockFooter from '~/releases/components/release_block_footer.vue';
const originalRelease = getJSONFixture('api/releases/release.json');
-const mockFutureDate = new Date(9999, 0, 0).toISOString();
-let mockIsFutureRelease = false;
-
-jest.mock('~/vue_shared/mixins/timeago', () => ({
- methods: {
- timeFormatted() {
- return mockIsFutureRelease ? 'in 1 month' : '7 fortnights ago';
- },
- tooltipTitle() {
- return 'February 30, 2401';
- },
- },
-}));
+// TODO: Encapsulate date helpers https://gitlab.com/gitlab-org/gitlab/-/issues/320883
+const MONTHS_IN_MS = 1000 * 60 * 60 * 24 * 31;
+const mockFutureDate = new Date(new Date().getTime() + MONTHS_IN_MS).toISOString();
describe('Release block footer', () => {
let wrapper;
@@ -44,7 +34,6 @@ describe('Release block footer', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
- mockIsFutureRelease = false;
});
const commitInfoSection = () => wrapper.find('.js-commit-info');
@@ -88,7 +77,7 @@ describe('Release block footer', () => {
it('renders the author and creation time info', () => {
expect(trimText(authorDateInfoSection().text())).toBe(
- `Created 7 fortnights ago by ${release.author.username}`,
+ `Created 1 year ago by ${release.author.username}`,
);
});
@@ -100,7 +89,6 @@ describe('Release block footer', () => {
describe('renders the author and creation time info with future release date', () => {
beforeEach(() => {
- mockIsFutureRelease = true;
factory({ releasedAt: mockFutureDate });
});
@@ -113,7 +101,6 @@ describe('Release block footer', () => {
describe('when the release date is in the future', () => {
beforeEach(() => {
- mockIsFutureRelease = true;
factory({ releasedAt: mockFutureDate });
});
@@ -177,13 +164,12 @@ describe('Release block footer', () => {
beforeEach(() => factory({ author: undefined }));
it('renders the release date without the author name', () => {
- expect(trimText(authorDateInfoSection().text())).toBe(`Created 7 fortnights ago`);
+ expect(trimText(authorDateInfoSection().text())).toBe(`Created 1 year ago`);
});
});
describe('future release without any author info', () => {
beforeEach(() => {
- mockIsFutureRelease = true;
factory({ author: undefined, releasedAt: mockFutureDate });
});
diff --git a/spec/frontend/releases/components/release_block_header_spec.js b/spec/frontend/releases/components/release_block_header_spec.js
index f2159871395..0f6657090e6 100644
--- a/spec/frontend/releases/components/release_block_header_spec.js
+++ b/spec/frontend/releases/components/release_block_header_spec.js
@@ -1,9 +1,9 @@
+import { GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { merge } from 'lodash';
-import { GlLink } from '@gitlab/ui';
import { getJSONFixture } from 'helpers/fixtures';
-import ReleaseBlockHeader from '~/releases/components/release_block_header.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import ReleaseBlockHeader from '~/releases/components/release_block_header.vue';
import { BACK_URL_PARAM } from '~/releases/constants';
const originalRelease = getJSONFixture('api/releases/release.json');
diff --git a/spec/frontend/releases/components/release_block_milestone_info_spec.js b/spec/frontend/releases/components/release_block_milestone_info_spec.js
index 4eb15e9da12..47fe10af946 100644
--- a/spec/frontend/releases/components/release_block_milestone_info_spec.js
+++ b/spec/frontend/releases/components/release_block_milestone_info_spec.js
@@ -1,10 +1,10 @@
-import { mount } from '@vue/test-utils';
import { GlProgressBar, GlLink, GlBadge, GlButton } from '@gitlab/ui';
-import { trimText } from 'helpers/text_helper';
+import { mount } from '@vue/test-utils';
import { getJSONFixture } from 'helpers/fixtures';
+import { trimText } from 'helpers/text_helper';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import ReleaseBlockMilestoneInfo from '~/releases/components/release_block_milestone_info.vue';
import { MAX_MILESTONES_TO_DISPLAY } from '~/releases/constants';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
const { milestones: originalMilestones } = getJSONFixture('api/releases/release.json');
diff --git a/spec/frontend/releases/components/release_block_spec.js b/spec/frontend/releases/components/release_block_spec.js
index 633c6690529..1ca441f7a5a 100644
--- a/spec/frontend/releases/components/release_block_spec.js
+++ b/spec/frontend/releases/components/release_block_spec.js
@@ -1,13 +1,13 @@
-import $ from 'jquery';
import { mount } from '@vue/test-utils';
+import $ from 'jquery';
import { getJSONFixture } from 'helpers/fixtures';
+import * as commonUtils from '~/lib/utils/common_utils';
+import * as urlUtility from '~/lib/utils/url_utility';
import EvidenceBlock from '~/releases/components/evidence_block.vue';
import ReleaseBlock from '~/releases/components/release_block.vue';
import ReleaseBlockFooter from '~/releases/components/release_block_footer.vue';
-import timeagoMixin from '~/vue_shared/mixins/timeago';
-import * as commonUtils from '~/lib/utils/common_utils';
import { BACK_URL_PARAM } from '~/releases/constants';
-import * as urlUtility from '~/lib/utils/url_utility';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
const originalRelease = getJSONFixture('api/releases/release.json');
diff --git a/spec/frontend/releases/components/release_skeleton_loader_spec.js b/spec/frontend/releases/components/release_skeleton_loader_spec.js
index 7fbf864568a..7f81081ff6c 100644
--- a/spec/frontend/releases/components/release_skeleton_loader_spec.js
+++ b/spec/frontend/releases/components/release_skeleton_loader_spec.js
@@ -1,5 +1,5 @@
-import { mount } from '@vue/test-utils';
import { GlSkeletonLoader } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import ReleaseSkeletonLoader from '~/releases/components/release_skeleton_loader.vue';
describe('release_skeleton_loader.vue', () => {
diff --git a/spec/frontend/releases/components/releases_pagination_graphql_spec.js b/spec/frontend/releases/components/releases_pagination_graphql_spec.js
index cee5e72e1c0..de80d82e93c 100644
--- a/spec/frontend/releases/components/releases_pagination_graphql_spec.js
+++ b/spec/frontend/releases/components/releases_pagination_graphql_spec.js
@@ -1,9 +1,9 @@
-import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+import { historyPushState } from '~/lib/utils/common_utils';
+import ReleasesPaginationGraphql from '~/releases/components/releases_pagination_graphql.vue';
import createStore from '~/releases/stores';
import createListModule from '~/releases/stores/modules/list';
-import ReleasesPaginationGraphql from '~/releases/components/releases_pagination_graphql.vue';
-import { historyPushState } from '~/lib/utils/common_utils';
jest.mock('~/lib/utils/common_utils', () => ({
...jest.requireActual('~/lib/utils/common_utils'),
diff --git a/spec/frontend/releases/components/releases_pagination_rest_spec.js b/spec/frontend/releases/components/releases_pagination_rest_spec.js
index b015792c96b..6f2690f5322 100644
--- a/spec/frontend/releases/components/releases_pagination_rest_spec.js
+++ b/spec/frontend/releases/components/releases_pagination_rest_spec.js
@@ -1,10 +1,10 @@
-import Vuex from 'vuex';
-import { mount, createLocalVue } from '@vue/test-utils';
import { GlPagination } from '@gitlab/ui';
+import { mount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+import * as commonUtils from '~/lib/utils/common_utils';
import ReleasesPaginationRest from '~/releases/components/releases_pagination_rest.vue';
import createStore from '~/releases/stores';
import createListModule from '~/releases/stores/modules/list';
-import * as commonUtils from '~/lib/utils/common_utils';
commonUtils.historyPushState = jest.fn();
diff --git a/spec/frontend/releases/components/releases_sort_spec.js b/spec/frontend/releases/components/releases_sort_spec.js
index c089ee3cc38..f17c6678592 100644
--- a/spec/frontend/releases/components/releases_sort_spec.js
+++ b/spec/frontend/releases/components/releases_sort_spec.js
@@ -1,6 +1,6 @@
-import Vuex from 'vuex';
import { GlSorting, GlSortingItem } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
import ReleasesSort from '~/releases/components/releases_sort.vue';
import createStore from '~/releases/stores';
import createListModule from '~/releases/stores/modules/list';
diff --git a/spec/frontend/releases/components/tag_field_exsting_spec.js b/spec/frontend/releases/components/tag_field_exsting_spec.js
index d4110b57776..cef7a0272a6 100644
--- a/spec/frontend/releases/components/tag_field_exsting_spec.js
+++ b/spec/frontend/releases/components/tag_field_exsting_spec.js
@@ -1,6 +1,6 @@
-import Vuex from 'vuex';
import { GlFormInput } from '@gitlab/ui';
import { shallowMount, mount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
import TagFieldExisting from '~/releases/components/tag_field_existing.vue';
import createStore from '~/releases/stores';
import createDetailModule from '~/releases/stores/modules/detail';
diff --git a/spec/frontend/releases/components/tag_field_new_spec.js b/spec/frontend/releases/components/tag_field_new_spec.js
index eba0e286b27..d87718138b8 100644
--- a/spec/frontend/releases/components/tag_field_new_spec.js
+++ b/spec/frontend/releases/components/tag_field_new_spec.js
@@ -1,9 +1,9 @@
-import { mount, shallowMount } from '@vue/test-utils';
import { GlFormInput } from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
+import RefSelector from '~/ref/components/ref_selector.vue';
import TagFieldNew from '~/releases/components/tag_field_new.vue';
import createStore from '~/releases/stores';
import createDetailModule from '~/releases/stores/modules/detail';
-import RefSelector from '~/ref/components/ref_selector.vue';
const TEST_TAG_NAME = 'test-tag-name';
const TEST_PROJECT_ID = '1234';
diff --git a/spec/frontend/releases/components/tag_field_spec.js b/spec/frontend/releases/components/tag_field_spec.js
index c7909a2369b..2cf5944f9e6 100644
--- a/spec/frontend/releases/components/tag_field_spec.js
+++ b/spec/frontend/releases/components/tag_field_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import TagField from '~/releases/components/tag_field.vue';
-import TagFieldNew from '~/releases/components/tag_field_new.vue';
import TagFieldExisting from '~/releases/components/tag_field_existing.vue';
+import TagFieldNew from '~/releases/components/tag_field_new.vue';
import createStore from '~/releases/stores';
import createDetailModule from '~/releases/stores/modules/detail';
diff --git a/spec/frontend/releases/stores/modules/detail/actions_spec.js b/spec/frontend/releases/stores/modules/detail/actions_spec.js
index 396e7bd8745..9c125fbb87b 100644
--- a/spec/frontend/releases/stores/modules/detail/actions_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/actions_spec.js
@@ -1,17 +1,17 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
-import testAction from 'helpers/vuex_action_helper';
-import { getJSONFixture } from 'helpers/fixtures';
import { cloneDeep } from 'lodash';
-import * as actions from '~/releases/stores/modules/detail/actions';
-import * as types from '~/releases/stores/modules/detail/mutation_types';
-import createState from '~/releases/stores/modules/detail/state';
+import { getJSONFixture } from 'helpers/fixtures';
+import testAction from 'helpers/vuex_action_helper';
+import api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import { redirectTo } from '~/lib/utils/url_utility';
-import api from '~/api';
import httpStatus from '~/lib/utils/http_status';
+import { redirectTo } from '~/lib/utils/url_utility';
import { ASSET_LINK_TYPE } from '~/releases/constants';
+import * as actions from '~/releases/stores/modules/detail/actions';
+import * as types from '~/releases/stores/modules/detail/mutation_types';
+import createState from '~/releases/stores/modules/detail/state';
import { releaseToApiJson, apiJsonToRelease } from '~/releases/util';
jest.mock('~/flash');
diff --git a/spec/frontend/releases/stores/modules/detail/mutations_spec.js b/spec/frontend/releases/stores/modules/detail/mutations_spec.js
index 88eddc4019c..cdf26bfa834 100644
--- a/spec/frontend/releases/stores/modules/detail/mutations_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/mutations_spec.js
@@ -1,9 +1,9 @@
import { getJSONFixture } from 'helpers/fixtures';
-import createState from '~/releases/stores/modules/detail/state';
-import mutations from '~/releases/stores/modules/detail/mutations';
-import * as types from '~/releases/stores/modules/detail/mutation_types';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { ASSET_LINK_TYPE, DEFAULT_ASSET_LINK_TYPE } from '~/releases/constants';
+import * as types from '~/releases/stores/modules/detail/mutation_types';
+import mutations from '~/releases/stores/modules/detail/mutations';
+import createState from '~/releases/stores/modules/detail/state';
const originalRelease = getJSONFixture('api/releases/release.json');
diff --git a/spec/frontend/releases/stores/modules/list/actions_spec.js b/spec/frontend/releases/stores/modules/list/actions_spec.js
index 35551b77dc4..309f7387929 100644
--- a/spec/frontend/releases/stores/modules/list/actions_spec.js
+++ b/spec/frontend/releases/stores/modules/list/actions_spec.js
@@ -1,6 +1,14 @@
import { cloneDeep } from 'lodash';
-import testAction from 'helpers/vuex_action_helper';
import { getJSONFixture } from 'helpers/fixtures';
+import testAction from 'helpers/vuex_action_helper';
+import api from '~/api';
+import {
+ normalizeHeaders,
+ parseIntPagination,
+ convertObjectPropsToCamelCase,
+} from '~/lib/utils/common_utils';
+import { PAGE_SIZE } from '~/releases/constants';
+import allReleasesQuery from '~/releases/queries/all_releases.query.graphql';
import {
fetchReleases,
fetchReleasesGraphQl,
@@ -8,18 +16,10 @@ import {
receiveReleasesError,
setSorting,
} from '~/releases/stores/modules/list/actions';
-import createState from '~/releases/stores/modules/list/state';
import * as types from '~/releases/stores/modules/list/mutation_types';
-import api from '~/api';
+import createState from '~/releases/stores/modules/list/state';
import { gqClient, convertAllReleasesGraphQLResponse } from '~/releases/util';
-import {
- normalizeHeaders,
- parseIntPagination,
- convertObjectPropsToCamelCase,
-} from '~/lib/utils/common_utils';
import { pageInfoHeadersWithoutPagination } from '../../../mock_data';
-import allReleasesQuery from '~/releases/queries/all_releases.query.graphql';
-import { PAGE_SIZE } from '~/releases/constants';
const originalRelease = getJSONFixture('api/releases/release.json');
const originalReleases = [originalRelease];
diff --git a/spec/frontend/releases/stores/modules/list/mutations_spec.js b/spec/frontend/releases/stores/modules/list/mutations_spec.js
index 78071573072..ea6a4ada16a 100644
--- a/spec/frontend/releases/stores/modules/list/mutations_spec.js
+++ b/spec/frontend/releases/stores/modules/list/mutations_spec.js
@@ -1,10 +1,10 @@
import { getJSONFixture } from 'helpers/fixtures';
-import createState from '~/releases/stores/modules/list/state';
-import mutations from '~/releases/stores/modules/list/mutations';
-import * as types from '~/releases/stores/modules/list/mutation_types';
import { parseIntPagination, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import { pageInfoHeadersWithoutPagination } from '../../../mock_data';
+import * as types from '~/releases/stores/modules/list/mutation_types';
+import mutations from '~/releases/stores/modules/list/mutations';
+import createState from '~/releases/stores/modules/list/state';
import { convertAllReleasesGraphQLResponse } from '~/releases/util';
+import { pageInfoHeadersWithoutPagination } from '../../../mock_data';
const originalRelease = getJSONFixture('api/releases/release.json');
const originalReleases = [originalRelease];
diff --git a/spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js b/spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js
index ccceb78f2d1..b716d54c9fc 100644
--- a/spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js
+++ b/spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js
@@ -1,7 +1,7 @@
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import GroupedAccessibilityReportsApp from '~/reports/accessibility_report/grouped_accessibility_reports_app.vue';
import AccessibilityIssueBody from '~/reports/accessibility_report/components/accessibility_issue_body.vue';
+import GroupedAccessibilityReportsApp from '~/reports/accessibility_report/grouped_accessibility_reports_app.vue';
import { getStoreConfig } from '~/reports/accessibility_report/store';
import { mockReport } from './mock_data';
diff --git a/spec/frontend/reports/accessibility_report/store/actions_spec.js b/spec/frontend/reports/accessibility_report/store/actions_spec.js
index 4e607fa5a76..46dbe1ff7a1 100644
--- a/spec/frontend/reports/accessibility_report/store/actions_spec.js
+++ b/spec/frontend/reports/accessibility_report/store/actions_spec.js
@@ -1,10 +1,10 @@
import MockAdapter from 'axios-mock-adapter';
-import { TEST_HOST } from 'spec/test_constants';
import testAction from 'helpers/vuex_action_helper';
+import { TEST_HOST } from 'spec/test_constants';
import axios from '~/lib/utils/axios_utils';
+import createStore from '~/reports/accessibility_report/store';
import * as actions from '~/reports/accessibility_report/store/actions';
import * as types from '~/reports/accessibility_report/store/mutation_types';
-import createStore from '~/reports/accessibility_report/store';
import { mockReport } from '../mock_data';
describe('Accessibility Reports actions', () => {
diff --git a/spec/frontend/reports/accessibility_report/store/getters_spec.js b/spec/frontend/reports/accessibility_report/store/getters_spec.js
index d74c71cfa09..96344596003 100644
--- a/spec/frontend/reports/accessibility_report/store/getters_spec.js
+++ b/spec/frontend/reports/accessibility_report/store/getters_spec.js
@@ -1,5 +1,5 @@
-import * as getters from '~/reports/accessibility_report/store/getters';
import createStore from '~/reports/accessibility_report/store';
+import * as getters from '~/reports/accessibility_report/store/getters';
import { LOADING, ERROR, SUCCESS, STATUS_FAILED } from '~/reports/constants';
describe('Accessibility reports store getters', () => {
diff --git a/spec/frontend/reports/accessibility_report/store/mutations_spec.js b/spec/frontend/reports/accessibility_report/store/mutations_spec.js
index a4e9571b721..b336261d804 100644
--- a/spec/frontend/reports/accessibility_report/store/mutations_spec.js
+++ b/spec/frontend/reports/accessibility_report/store/mutations_spec.js
@@ -1,5 +1,5 @@
-import mutations from '~/reports/accessibility_report/store/mutations';
import createStore from '~/reports/accessibility_report/store';
+import mutations from '~/reports/accessibility_report/store/mutations';
describe('Accessibility Reports mutations', () => {
let localState;
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
index ecb657af6f1..f0b23bb7b58 100644
--- a/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js
+++ b/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js
@@ -1,7 +1,7 @@
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import GroupedCodequalityReportsApp from '~/reports/codequality_report/grouped_codequality_reports_app.vue';
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 { mockParsedHeadIssues, mockParsedBaseIssues } from './mock_data';
@@ -9,7 +9,6 @@ const localVue = createLocalVue();
localVue.use(Vuex);
describe('Grouped code quality reports app', () => {
- const Component = localVue.extend(GroupedCodequalityReportsApp);
let wrapper;
let mockStore;
@@ -22,7 +21,7 @@ describe('Grouped code quality reports app', () => {
};
const mountComponent = (props = {}) => {
- wrapper = mount(Component, {
+ wrapper = mount(GroupedCodequalityReportsApp, {
store: mockStore,
localVue,
propsData: {
@@ -135,7 +134,7 @@ describe('Grouped code quality reports app', () => {
});
it('renders error text', () => {
- expect(findWidget().text()).toEqual('Failed to load codeclimate report');
+ expect(findWidget().text()).toContain('Failed to load codeclimate report');
});
it('renders a help icon with more information', () => {
diff --git a/spec/frontend/reports/codequality_report/mock_data.js b/spec/frontend/reports/codequality_report/mock_data.js
index 9bd61527d3f..c5cecb34509 100644
--- a/spec/frontend/reports/codequality_report/mock_data.js
+++ b/spec/frontend/reports/codequality_report/mock_data.js
@@ -88,3 +88,53 @@ export const issueDiff = [
urlPath: 'headPath/lib/six.rb#L6',
},
];
+
+export const reportIssues = {
+ status: 'failed',
+ new_errors: [
+ {
+ description:
+ 'Method `long_if` has a Cognitive Complexity of 10 (exceeds 5 allowed). Consider refactoring.',
+ severity: 'minor',
+ file_path: 'codequality.rb',
+ line: 5,
+ },
+ ],
+ resolved_errors: [
+ {
+ description: 'Insecure Dependency',
+ severity: 'major',
+ file_path: 'lib/six.rb',
+ line: 22,
+ },
+ ],
+ existing_errors: [],
+ summary: { total: 3, resolved: 0, errored: 3 },
+};
+
+export const parsedReportIssues = {
+ newIssues: [
+ {
+ description:
+ 'Method `long_if` has a Cognitive Complexity of 10 (exceeds 5 allowed). Consider refactoring.',
+ file_path: 'codequality.rb',
+ line: 5,
+ name:
+ 'Method `long_if` has a Cognitive Complexity of 10 (exceeds 5 allowed). Consider refactoring.',
+ path: 'codequality.rb',
+ severity: 'minor',
+ urlPath: 'null/codequality.rb#L5',
+ },
+ ],
+ resolvedIssues: [
+ {
+ description: 'Insecure Dependency',
+ file_path: 'lib/six.rb',
+ line: 22,
+ name: 'Insecure Dependency',
+ path: 'lib/six.rb',
+ severity: 'major',
+ urlPath: 'null/lib/six.rb#L22',
+ },
+ ],
+};
diff --git a/spec/frontend/reports/codequality_report/store/actions_spec.js b/spec/frontend/reports/codequality_report/store/actions_spec.js
index 321785cb85a..a2b256448ef 100644
--- a/spec/frontend/reports/codequality_report/store/actions_spec.js
+++ b/spec/frontend/reports/codequality_report/store/actions_spec.js
@@ -1,11 +1,18 @@
import MockAdapter from 'axios-mock-adapter';
-import { TEST_HOST } from 'spec/test_constants';
import testAction from 'helpers/vuex_action_helper';
+import { TEST_HOST } from 'spec/test_constants';
import axios from '~/lib/utils/axios_utils';
+import createStore from '~/reports/codequality_report/store';
import * as actions from '~/reports/codequality_report/store/actions';
import * as types from '~/reports/codequality_report/store/mutation_types';
-import createStore from '~/reports/codequality_report/store';
-import { headIssues, baseIssues, mockParsedHeadIssues, mockParsedBaseIssues } from '../mock_data';
+import {
+ headIssues,
+ baseIssues,
+ mockParsedHeadIssues,
+ mockParsedBaseIssues,
+ reportIssues,
+ parsedReportIssues,
+} from '../mock_data';
// mock codequality comparison worker
jest.mock('~/reports/codequality_report/workers/codequality_comparison_worker', () =>
@@ -39,6 +46,7 @@ describe('Codequality Reports actions', () => {
headPath: 'headPath',
baseBlobPath: 'baseBlobPath',
headBlobPath: 'headBlobPath',
+ reportsPath: 'reportsPath',
helpPath: 'codequalityHelpPath',
};
@@ -55,68 +63,119 @@ describe('Codequality Reports actions', () => {
describe('fetchReports', () => {
let mock;
+ let diffFeatureFlagEnabled;
- beforeEach(() => {
- localState.headPath = `${TEST_HOST}/head.json`;
- localState.basePath = `${TEST_HOST}/base.json`;
- mock = new MockAdapter(axios);
- });
+ describe('with codequalityBackendComparison feature flag enabled', () => {
+ beforeEach(() => {
+ diffFeatureFlagEnabled = true;
+ localState.reportsPath = `${TEST_HOST}/codequality_reports.json`;
+ mock = new MockAdapter(axios);
+ });
- afterEach(() => {
- mock.restore();
- });
+ afterEach(() => {
+ mock.restore();
+ });
- describe('on success', () => {
- it('commits REQUEST_REPORTS and dispatches receiveReportsSuccess', (done) => {
- mock.onGet(`${TEST_HOST}/head.json`).reply(200, headIssues);
- mock.onGet(`${TEST_HOST}/base.json`).reply(200, baseIssues);
-
- testAction(
- actions.fetchReports,
- null,
- localState,
- [{ type: types.REQUEST_REPORTS }],
- [
- {
- payload: {
- newIssues: [mockParsedHeadIssues[0]],
- resolvedIssues: [mockParsedBaseIssues[0]],
+ describe('on success', () => {
+ it('commits REQUEST_REPORTS and dispatches receiveReportsSuccess', (done) => {
+ mock.onGet(`${TEST_HOST}/codequality_reports.json`).reply(200, reportIssues);
+
+ testAction(
+ actions.fetchReports,
+ diffFeatureFlagEnabled,
+ localState,
+ [{ type: types.REQUEST_REPORTS }],
+ [
+ {
+ payload: parsedReportIssues,
+ type: 'receiveReportsSuccess',
},
- type: 'receiveReportsSuccess',
- },
- ],
- done,
- );
+ ],
+ done,
+ );
+ });
});
- });
- describe('on error', () => {
- it('commits REQUEST_REPORTS and dispatches receiveReportsError', (done) => {
- mock.onGet(`${TEST_HOST}/head.json`).reply(500);
-
- testAction(
- actions.fetchReports,
- null,
- localState,
- [{ type: types.REQUEST_REPORTS }],
- [{ type: 'receiveReportsError' }],
- done,
- );
+ describe('on error', () => {
+ it('commits REQUEST_REPORTS and dispatches receiveReportsError', (done) => {
+ mock.onGet(`${TEST_HOST}/codequality_reports.json`).reply(500);
+
+ testAction(
+ actions.fetchReports,
+ diffFeatureFlagEnabled,
+ localState,
+ [{ type: types.REQUEST_REPORTS }],
+ [{ type: 'receiveReportsError', payload: expect.any(Error) }],
+ done,
+ );
+ });
});
});
- describe('with no base path', () => {
- it('commits REQUEST_REPORTS and dispatches receiveReportsError', (done) => {
- localState.basePath = null;
-
- testAction(
- actions.fetchReports,
- null,
- localState,
- [{ type: types.REQUEST_REPORTS }],
- [{ type: 'receiveReportsError' }],
- done,
- );
+ describe('with codequalityBackendComparison feature flag disabled', () => {
+ beforeEach(() => {
+ diffFeatureFlagEnabled = false;
+ localState.headPath = `${TEST_HOST}/head.json`;
+ localState.basePath = `${TEST_HOST}/base.json`;
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('on success', () => {
+ it('commits REQUEST_REPORTS and dispatches receiveReportsSuccess', (done) => {
+ mock.onGet(`${TEST_HOST}/head.json`).reply(200, headIssues);
+ mock.onGet(`${TEST_HOST}/base.json`).reply(200, baseIssues);
+
+ testAction(
+ actions.fetchReports,
+ diffFeatureFlagEnabled,
+ localState,
+ [{ type: types.REQUEST_REPORTS }],
+ [
+ {
+ payload: {
+ newIssues: [mockParsedHeadIssues[0]],
+ resolvedIssues: [mockParsedBaseIssues[0]],
+ },
+ type: 'receiveReportsSuccess',
+ },
+ ],
+ done,
+ );
+ });
+ });
+
+ describe('on error', () => {
+ it('commits REQUEST_REPORTS and dispatches receiveReportsError', (done) => {
+ mock.onGet(`${TEST_HOST}/head.json`).reply(500);
+
+ testAction(
+ actions.fetchReports,
+ diffFeatureFlagEnabled,
+ localState,
+ [{ type: types.REQUEST_REPORTS }],
+ [{ type: 'receiveReportsError', payload: expect.any(Error) }],
+ done,
+ );
+ });
+ });
+
+ describe('with no base path', () => {
+ it('commits REQUEST_REPORTS and dispatches receiveReportsError', (done) => {
+ localState.basePath = null;
+
+ testAction(
+ actions.fetchReports,
+ diffFeatureFlagEnabled,
+ localState,
+ [{ type: types.REQUEST_REPORTS }],
+ [{ type: 'receiveReportsError' }],
+ done,
+ );
+ });
});
});
});
@@ -142,7 +201,7 @@ describe('Codequality Reports actions', () => {
actions.receiveReportsError,
null,
localState,
- [{ type: types.RECEIVE_REPORTS_ERROR }],
+ [{ type: types.RECEIVE_REPORTS_ERROR, payload: null }],
[],
done,
);
diff --git a/spec/frontend/reports/codequality_report/store/getters_spec.js b/spec/frontend/reports/codequality_report/store/getters_spec.js
index a641e2fe74f..de025f814ef 100644
--- a/spec/frontend/reports/codequality_report/store/getters_spec.js
+++ b/spec/frontend/reports/codequality_report/store/getters_spec.js
@@ -1,5 +1,5 @@
-import * as getters from '~/reports/codequality_report/store/getters';
import createStore from '~/reports/codequality_report/store';
+import * as getters from '~/reports/codequality_report/store/getters';
import { LOADING, ERROR, SUCCESS } from '~/reports/constants';
describe('Codequality reports store getters', () => {
diff --git a/spec/frontend/reports/codequality_report/store/mutations_spec.js b/spec/frontend/reports/codequality_report/store/mutations_spec.js
index 658abf3088c..05a16cd6f82 100644
--- a/spec/frontend/reports/codequality_report/store/mutations_spec.js
+++ b/spec/frontend/reports/codequality_report/store/mutations_spec.js
@@ -1,5 +1,5 @@
-import mutations from '~/reports/codequality_report/store/mutations';
import createStore from '~/reports/codequality_report/store';
+import mutations from '~/reports/codequality_report/store/mutations';
describe('Codequality Reports mutations', () => {
let localState;
@@ -55,6 +55,12 @@ describe('Codequality Reports mutations', () => {
expect(localState.hasError).toEqual(false);
});
+ it('clears statusReason', () => {
+ mutations.RECEIVE_REPORTS_SUCCESS(localState, {});
+
+ expect(localState.statusReason).toEqual('');
+ });
+
it('sets newIssues and resolvedIssues from response data', () => {
const data = { newIssues: [{ id: 1 }], resolvedIssues: [{ id: 2 }] };
mutations.RECEIVE_REPORTS_SUCCESS(localState, data);
@@ -76,5 +82,13 @@ describe('Codequality Reports mutations', () => {
expect(localState.hasError).toEqual(true);
});
+
+ it('sets statusReason to string from error response data', () => {
+ const data = { status_reason: 'This merge request does not have codequality reports' };
+ const error = { response: { data } };
+ mutations.RECEIVE_REPORTS_ERROR(localState, error);
+
+ expect(localState.statusReason).toEqual(data.status_reason);
+ });
});
});
diff --git a/spec/frontend/reports/codequality_report/store/utils/codequality_comparison_spec.js b/spec/frontend/reports/codequality_report/store/utils/codequality_comparison_spec.js
index 085d697672d..389e9b4a1f6 100644
--- a/spec/frontend/reports/codequality_report/store/utils/codequality_comparison_spec.js
+++ b/spec/frontend/reports/codequality_report/store/utils/codequality_comparison_spec.js
@@ -2,7 +2,13 @@ import {
parseCodeclimateMetrics,
doCodeClimateComparison,
} from '~/reports/codequality_report/store/utils/codequality_comparison';
-import { baseIssues, mockParsedHeadIssues, mockParsedBaseIssues } from '../../mock_data';
+import {
+ baseIssues,
+ mockParsedHeadIssues,
+ mockParsedBaseIssues,
+ reportIssues,
+ parsedReportIssues,
+} from '../../mock_data';
jest.mock('~/reports/codequality_report/workers/codequality_comparison_worker', () => {
let mockPostMessageCallback;
@@ -34,7 +40,7 @@ describe('Codequality report store utils', () => {
let result;
describe('parseCodeclimateMetrics', () => {
- it('should parse the received issues', () => {
+ it('should parse the issues from codeclimate artifacts', () => {
[result] = parseCodeclimateMetrics(baseIssues, 'path');
expect(result.name).toEqual(baseIssues[0].check_name);
@@ -42,6 +48,14 @@ describe('Codequality report store utils', () => {
expect(result.line).toEqual(baseIssues[0].location.lines.begin);
});
+ it('should parse the issues from backend codequality diff', () => {
+ [result] = parseCodeclimateMetrics(reportIssues.new_errors, 'path');
+
+ expect(result.name).toEqual(parsedReportIssues.newIssues[0].name);
+ expect(result.path).toEqual(parsedReportIssues.newIssues[0].path);
+ expect(result.line).toEqual(parsedReportIssues.newIssues[0].line);
+ });
+
describe('when an issue has no location or path', () => {
const issue = { description: 'Insecure Dependency' };
diff --git a/spec/frontend/reports/components/grouped_test_reports_app_spec.js b/spec/frontend/reports/components/grouped_test_reports_app_spec.js
index 492192988fb..ed261ed12c0 100644
--- a/spec/frontend/reports/components/grouped_test_reports_app_spec.js
+++ b/spec/frontend/reports/components/grouped_test_reports_app_spec.js
@@ -5,11 +5,11 @@ import GroupedTestReportsApp from '~/reports/components/grouped_test_reports_app
import { getStoreConfig } from '~/reports/store';
import { failedReport } from '../mock_data/mock_data';
-import successTestReports from '../mock_data/no_failures_report.json';
+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 newErrorsTestReports from '../mock_data/new_errors_report.json';
-import mixedResultsTestReports from '../mock_data/new_and_fixed_failures_report.json';
import resolvedFailures from '../mock_data/resolved_failures.json';
const localVue = createLocalVue();
@@ -18,12 +18,11 @@ localVue.use(Vuex);
describe('Grouped test reports app', () => {
const endpoint = 'endpoint.json';
const pipelinePath = '/path/to/pipeline';
- const Component = localVue.extend(GroupedTestReportsApp);
let wrapper;
let mockStore;
const mountComponent = ({ props = { pipelinePath } } = {}) => {
- wrapper = mount(Component, {
+ wrapper = mount(GroupedTestReportsApp, {
store: mockStore,
localVue,
propsData: {
diff --git a/spec/frontend/reports/components/modal_spec.js b/spec/frontend/reports/components/modal_spec.js
index 39b84d1ee05..d47bb964e8a 100644
--- a/spec/frontend/reports/components/modal_spec.js
+++ b/spec/frontend/reports/components/modal_spec.js
@@ -2,9 +2,9 @@ import { GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import CodeBlock from '~/vue_shared/components/code_block.vue';
import ReportsModal from '~/reports/components/modal.vue';
import state from '~/reports/store/state';
+import CodeBlock from '~/vue_shared/components/code_block.vue';
const StubbedGlModal = { template: '<div><slot></slot></div>', name: 'GlModal', props: ['title'] };
diff --git a/spec/frontend/reports/components/report_item_spec.js b/spec/frontend/reports/components/report_item_spec.js
index 6aac07984e3..a7243c5377b 100644
--- a/spec/frontend/reports/components/report_item_spec.js
+++ b/spec/frontend/reports/components/report_item_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
-import { STATUS_SUCCESS } from '~/reports/constants';
-import ReportItem from '~/reports/components/report_item.vue';
-import IssueStatusIcon from '~/reports/components/issue_status_icon.vue';
import { componentNames } from '~/reports/components/issue_body';
+import IssueStatusIcon from '~/reports/components/issue_status_icon.vue';
+import ReportItem from '~/reports/components/report_item.vue';
+import { STATUS_SUCCESS } from '~/reports/constants';
describe('ReportItem', () => {
describe('showReportSectionStatusIcon', () => {
diff --git a/spec/frontend/reports/components/report_section_spec.js b/spec/frontend/reports/components/report_section_spec.js
index c3219b34057..c9bf3185f8f 100644
--- a/spec/frontend/reports/components/report_section_spec.js
+++ b/spec/frontend/reports/components/report_section_spec.js
@@ -1,5 +1,5 @@
-import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import mountComponent, { mountComponentWithSlots } from 'helpers/vue_mount_component_helper';
import reportSection from '~/reports/components/report_section.vue';
diff --git a/spec/frontend/reports/components/summary_row_spec.js b/spec/frontend/reports/components/summary_row_spec.js
index 85c68ed069b..bdd6de1e0be 100644
--- a/spec/frontend/reports/components/summary_row_spec.js
+++ b/spec/frontend/reports/components/summary_row_spec.js
@@ -32,7 +32,7 @@ describe('Summary row', () => {
it('renders provided summary', () => {
createComponent();
- expect(findSummary().text()).toEqual(props.summary);
+ expect(findSummary().text()).toContain(props.summary);
});
it('renders provided icon', () => {
@@ -48,7 +48,7 @@ describe('Summary row', () => {
createComponent({ slots: { summary: summarySlotContent } });
expect(wrapper.text()).not.toContain(props.summary);
- expect(findSummary().text()).toEqual(summarySlotContent);
+ expect(findSummary().text()).toContain(summarySlotContent);
});
});
});
diff --git a/spec/frontend/reports/components/test_issue_body_spec.js b/spec/frontend/reports/components/test_issue_body_spec.js
index c13a3599fef..2843620a18d 100644
--- a/spec/frontend/reports/components/test_issue_body_spec.js
+++ b/spec/frontend/reports/components/test_issue_body_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import { trimText } from 'helpers/text_helper';
+import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import component from '~/reports/components/test_issue_body.vue';
import createStore from '~/reports/store';
import { issue } from '../mock_data/mock_data';
diff --git a/spec/frontend/reports/store/actions_spec.js b/spec/frontend/reports/store/actions_spec.js
index b7c4a31b1c8..25c3105466f 100644
--- a/spec/frontend/reports/store/actions_spec.js
+++ b/spec/frontend/reports/store/actions_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
-import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'helpers/test_constants';
+import testAction from 'helpers/vuex_action_helper';
import axios from '~/lib/utils/axios_utils';
import {
setEndpoint,
@@ -13,8 +13,8 @@ import {
openModal,
closeModal,
} from '~/reports/store/actions';
-import state from '~/reports/store/state';
import * as types from '~/reports/store/mutation_types';
+import state from '~/reports/store/state';
describe('Reports Store Actions', () => {
let mockedState;
diff --git a/spec/frontend/reports/store/mutations_spec.js b/spec/frontend/reports/store/mutations_spec.js
index 59065d1151f..652b3b0ec45 100644
--- a/spec/frontend/reports/store/mutations_spec.js
+++ b/spec/frontend/reports/store/mutations_spec.js
@@ -1,6 +1,6 @@
-import state from '~/reports/store/state';
-import mutations from '~/reports/store/mutations';
import * as types from '~/reports/store/mutation_types';
+import mutations from '~/reports/store/mutations';
+import state from '~/reports/store/state';
import { issue } from '../mock_data/mock_data';
describe('Reports Store Mutations', () => {
diff --git a/spec/frontend/reports/store/utils_spec.js b/spec/frontend/reports/store/utils_spec.js
index 5249e9ffcce..cbc87bbb5ec 100644
--- a/spec/frontend/reports/store/utils_spec.js
+++ b/spec/frontend/reports/store/utils_spec.js
@@ -1,4 +1,3 @@
-import * as utils from '~/reports/store/utils';
import {
STATUS_FAILED,
STATUS_SUCCESS,
@@ -6,6 +5,7 @@ import {
ICON_SUCCESS,
ICON_NOTFOUND,
} from '~/reports/constants';
+import * as utils from '~/reports/store/utils';
describe('Reports store utils', () => {
describe('summaryTextbuilder', () => {
diff --git a/spec/frontend/repository/components/breadcrumbs_spec.js b/spec/frontend/repository/components/breadcrumbs_spec.js
index a0dc608ddc9..2ac2069a177 100644
--- a/spec/frontend/repository/components/breadcrumbs_spec.js
+++ b/spec/frontend/repository/components/breadcrumbs_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount, RouterLinkStub } from '@vue/test-utils';
import { GlDropdown } from '@gitlab/ui';
+import { shallowMount, RouterLinkStub } from '@vue/test-utils';
import Breadcrumbs from '~/repository/components/breadcrumbs.vue';
let vm;
diff --git a/spec/frontend/repository/components/last_commit_spec.js b/spec/frontend/repository/components/last_commit_spec.js
index fe77057c3d4..ebea7dde34a 100644
--- a/spec/frontend/repository/components/last_commit_spec.js
+++ b/spec/frontend/repository/components/last_commit_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import LastCommit from '~/repository/components/last_commit.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
@@ -58,77 +58,75 @@ describe('Repository last commit component', () => {
loading | label
${true} | ${'shows'}
${false} | ${'hides'}
- `('$label when loading icon $loading is true', ({ loading }) => {
+ `('$label when loading icon $loading is true', async ({ loading }) => {
factory(createCommitData(), loading);
- return vm.vm.$nextTick(() => {
- expect(vm.find(GlLoadingIcon).exists()).toBe(loading);
- });
+ await vm.vm.$nextTick();
+
+ expect(vm.find(GlLoadingIcon).exists()).toBe(loading);
});
- it('renders commit widget', () => {
+ it('renders commit widget', async () => {
factory();
- return vm.vm.$nextTick(() => {
- expect(vm.element).toMatchSnapshot();
- });
+ await vm.vm.$nextTick();
+
+ expect(vm.element).toMatchSnapshot();
});
- it('renders short commit ID', () => {
+ it('renders short commit ID', async () => {
factory();
- return vm.vm.$nextTick(() => {
- expect(vm.find('[data-testid="last-commit-id-label"]').text()).toEqual('12345678');
- });
+ await vm.vm.$nextTick();
+
+ expect(vm.find('[data-testid="last-commit-id-label"]').text()).toEqual('12345678');
});
- it('hides pipeline components when pipeline does not exist', () => {
+ it('hides pipeline components when pipeline does not exist', async () => {
factory(createCommitData({ pipeline: null }));
- return vm.vm.$nextTick(() => {
- expect(vm.find('.js-commit-pipeline').exists()).toBe(false);
- });
+ await vm.vm.$nextTick();
+
+ expect(vm.find('.js-commit-pipeline').exists()).toBe(false);
});
- it('renders pipeline components', () => {
+ it('renders pipeline components', async () => {
factory();
- return vm.vm.$nextTick(() => {
- expect(vm.find('.js-commit-pipeline').exists()).toBe(true);
- });
+ await vm.vm.$nextTick();
+
+ expect(vm.find('.js-commit-pipeline').exists()).toBe(true);
});
- it('hides author component when author does not exist', () => {
+ it('hides author component when author does not exist', async () => {
factory(createCommitData({ author: null }));
- return vm.vm.$nextTick(() => {
- expect(vm.find('.js-user-link').exists()).toBe(false);
- expect(vm.find(UserAvatarLink).exists()).toBe(false);
- });
+ await vm.vm.$nextTick();
+
+ expect(vm.find('.js-user-link').exists()).toBe(false);
+ expect(vm.find(UserAvatarLink).exists()).toBe(false);
});
- it('does not render description expander when description is null', () => {
+ it('does not render description expander when description is null', async () => {
factory(createCommitData({ descriptionHtml: null }));
- return vm.vm.$nextTick(() => {
- expect(vm.find('.text-expander').exists()).toBe(false);
- expect(vm.find('.commit-row-description').exists()).toBe(false);
- });
+ await vm.vm.$nextTick();
+
+ expect(vm.find('.text-expander').exists()).toBe(false);
+ expect(vm.find('.commit-row-description').exists()).toBe(false);
});
- it('expands commit description when clicking expander', () => {
+ it('expands commit description when clicking expander', async () => {
factory(createCommitData({ descriptionHtml: 'Test description' }));
- return vm.vm
- .$nextTick()
- .then(() => {
- vm.find('.text-expander').vm.$emit('click');
- return vm.vm.$nextTick();
- })
- .then(() => {
- expect(vm.find('.commit-row-description').isVisible()).toBe(true);
- expect(vm.find('.text-expander').classes('open')).toBe(true);
- });
+ await vm.vm.$nextTick();
+
+ vm.find('.text-expander').vm.$emit('click');
+
+ await vm.vm.$nextTick();
+
+ expect(vm.find('.commit-row-description').isVisible()).toBe(true);
+ expect(vm.find('.text-expander').classes('open')).toBe(true);
});
it('strips the first newline of the description', async () => {
@@ -141,19 +139,19 @@ describe('Repository last commit component', () => {
);
});
- it('renders the signature HTML as returned by the backend', () => {
+ it('renders the signature HTML as returned by the backend', async () => {
factory(createCommitData({ signatureHtml: '<button>Verified</button>' }));
- return vm.vm.$nextTick().then(() => {
- expect(vm.element).toMatchSnapshot();
- });
+ await vm.vm.$nextTick();
+
+ expect(vm.element).toMatchSnapshot();
});
- it('sets correct CSS class if the commit message is empty', () => {
+ it('sets correct CSS class if the commit message is empty', async () => {
factory(createCommitData({ message: '' }));
- return vm.vm.$nextTick().then(() => {
- expect(vm.find('.item-title').classes()).toContain(emptyMessageClass);
- });
+ await vm.vm.$nextTick();
+
+ expect(vm.find('.item-title').classes()).toContain(emptyMessageClass);
});
});
diff --git a/spec/frontend/repository/components/preview/index_spec.js b/spec/frontend/repository/components/preview/index_spec.js
index ebd985e640c..466eed52739 100644
--- a/spec/frontend/repository/components/preview/index_spec.js
+++ b/spec/frontend/repository/components/preview/index_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { handleLocationHash } from '~/lib/utils/common_utils';
import Preview from '~/repository/components/preview/index.vue';
diff --git a/spec/frontend/repository/components/table/index_spec.js b/spec/frontend/repository/components/table/index_spec.js
index 1b8bbd5af6b..af263f43d7d 100644
--- a/spec/frontend/repository/components/table/index_spec.js
+++ b/spec/frontend/repository/components/table/index_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import Table from '~/repository/components/table/index.vue';
import TableRow from '~/repository/components/table/row.vue';
diff --git a/spec/frontend/repository/components/table/parent_row_spec.js b/spec/frontend/repository/components/table/parent_row_spec.js
index b4800112fee..cf1ed272634 100644
--- a/spec/frontend/repository/components/table/parent_row_spec.js
+++ b/spec/frontend/repository/components/table/parent_row_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount, RouterLinkStub } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount, RouterLinkStub } from '@vue/test-utils';
import ParentRow from '~/repository/components/table/parent_row.vue';
let vm;
diff --git a/spec/frontend/repository/components/table/row_spec.js b/spec/frontend/repository/components/table/row_spec.js
index 767b117c798..69cb69de5df 100644
--- a/spec/frontend/repository/components/table/row_spec.js
+++ b/spec/frontend/repository/components/table/row_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount, RouterLinkStub } from '@vue/test-utils';
import { GlBadge, GlLink, GlIcon } from '@gitlab/ui';
+import { shallowMount, RouterLinkStub } from '@vue/test-utils';
import TableRow from '~/repository/components/table/row.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import { FILE_SYMLINK_MODE } from '~/vue_shared/constants';
diff --git a/spec/frontend/repository/components/tree_content_spec.js b/spec/frontend/repository/components/tree_content_spec.js
index 70dbfaea551..2930e39df8a 100644
--- a/spec/frontend/repository/components/tree_content_spec.js
+++ b/spec/frontend/repository/components/tree_content_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
-import TreeContent, { INITIAL_FETCH_COUNT } from '~/repository/components/tree_content.vue';
import FilePreview from '~/repository/components/preview/index.vue';
import FileTable from '~/repository/components/table/index.vue';
+import TreeContent, { INITIAL_FETCH_COUNT } from '~/repository/components/tree_content.vue';
let vm;
let $apollo;
diff --git a/spec/frontend/repository/utils/dom_spec.js b/spec/frontend/repository/utils/dom_spec.js
index f9e619a82d1..9839ddc6cdb 100644
--- a/spec/frontend/repository/utils/dom_spec.js
+++ b/spec/frontend/repository/utils/dom_spec.js
@@ -1,5 +1,5 @@
-import { TEST_HOST } from 'helpers/test_constants';
import { setHTMLFixture } from 'helpers/fixtures';
+import { TEST_HOST } from 'helpers/test_constants';
import { updateElementsVisibility, updateFormAction } from '~/repository/utils/dom';
describe('updateElementsVisibility', () => {
diff --git a/spec/frontend/right_sidebar_spec.js b/spec/frontend/right_sidebar_spec.js
index 1b31030cb92..f3719b28baa 100644
--- a/spec/frontend/right_sidebar_spec.js
+++ b/spec/frontend/right_sidebar_spec.js
@@ -1,5 +1,5 @@
-import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter';
+import $ from 'jquery';
import '~/commons/bootstrap';
import axios from '~/lib/utils/axios_utils';
import Sidebar from '~/right_sidebar';
diff --git a/spec/frontend/search/highlight_blob_search_result_spec.js b/spec/frontend/search/highlight_blob_search_result_spec.js
index 112e6f5124f..c1b0c7d794b 100644
--- a/spec/frontend/search/highlight_blob_search_result_spec.js
+++ b/spec/frontend/search/highlight_blob_search_result_spec.js
@@ -1,6 +1,7 @@
import setHighlightClass from '~/search/highlight_blob_search_result';
const fixture = 'search/blob_search_result.html';
+const searchKeyword = 'Send'; // spec/frontend/fixtures/search.rb#79
describe('search/highlight_blob_search_result', () => {
preloadFixtures(fixture);
@@ -8,7 +9,7 @@ describe('search/highlight_blob_search_result', () => {
beforeEach(() => loadFixtures(fixture));
it('highlights lines with search term occurrence', () => {
- setHighlightClass();
+ setHighlightClass(searchKeyword);
expect(document.querySelectorAll('.blob-result .hll').length).toBe(4);
});
diff --git a/spec/frontend/search/index_spec.js b/spec/frontend/search/index_spec.js
index 023cd341345..1992a7f4437 100644
--- a/spec/frontend/search/index_spec.js
+++ b/spec/frontend/search/index_spec.js
@@ -1,9 +1,11 @@
+import setHighlightClass from 'ee_else_ce/search/highlight_blob_search_result';
import { initSearchApp } from '~/search';
import createStore from '~/search/store';
jest.mock('~/search/store');
jest.mock('~/search/topbar');
jest.mock('~/search/sidebar');
+jest.mock('ee_else_ce/search/highlight_blob_search_result');
describe('initSearchApp', () => {
let defaultLocation;
@@ -42,6 +44,7 @@ describe('initSearchApp', () => {
it(`decodes ${search} to ${decodedSearch}`, () => {
expect(createStore).toHaveBeenCalledWith({ query: { search: decodedSearch } });
+ expect(setHighlightClass).toHaveBeenCalledWith(decodedSearch);
});
});
});
diff --git a/spec/frontend/search/mock_data.js b/spec/frontend/search/mock_data.js
index ee509eaad8d..d076997b04a 100644
--- a/spec/frontend/search/mock_data.js
+++ b/spec/frontend/search/mock_data.js
@@ -26,7 +26,7 @@ export const MOCK_GROUPS = [
export const MOCK_PROJECT = {
name: 'test project',
- namespace_id: MOCK_GROUP.id,
+ namespace: MOCK_GROUP,
nameWithNamespace: 'test group test project',
id: 'test_1',
};
@@ -34,14 +34,30 @@ export const MOCK_PROJECT = {
export const MOCK_PROJECTS = [
{
name: 'test project',
- namespace_id: MOCK_GROUP.id,
+ namespace: MOCK_GROUP,
name_with_namespace: 'test group test project',
id: 'test_1',
},
{
name: 'test project 2',
- namespace_id: MOCK_GROUP.id,
+ namespace: MOCK_GROUP,
name_with_namespace: 'test group test project 2',
id: 'test_2',
},
];
+
+export const MOCK_SORT_OPTIONS = [
+ {
+ title: 'Most relevant',
+ sortable: false,
+ sortParam: 'relevant',
+ },
+ {
+ title: 'Created date',
+ sortable: true,
+ sortParam: {
+ asc: 'created_asc',
+ desc: 'created_desc',
+ },
+ },
+];
diff --git a/spec/frontend/search/sidebar/components/app_spec.js b/spec/frontend/search/sidebar/components/app_spec.js
index 94a39b90d02..b93527c1fe9 100644
--- a/spec/frontend/search/sidebar/components/app_spec.js
+++ b/spec/frontend/search/sidebar/components/app_spec.js
@@ -1,6 +1,6 @@
-import Vuex from 'vuex';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlButton, GlLink } from '@gitlab/ui';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+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';
diff --git a/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js b/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js
index 42fcc859308..3713e1d414f 100644
--- a/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js
+++ b/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js
@@ -1,5 +1,5 @@
-import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
+import Vuex from 'vuex';
import { MOCK_QUERY } from 'jest/search/mock_data';
import ConfidentialityFilter from '~/search/sidebar/components/confidentiality_filter.vue';
import RadioFilter from '~/search/sidebar/components/radio_filter.vue';
diff --git a/spec/frontend/search/sidebar/components/radio_filter_spec.js b/spec/frontend/search/sidebar/components/radio_filter_spec.js
index 9918af54cfe..4c81312e479 100644
--- a/spec/frontend/search/sidebar/components/radio_filter_spec.js
+++ b/spec/frontend/search/sidebar/components/radio_filter_spec.js
@@ -1,10 +1,10 @@
-import Vuex from 'vuex';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlFormRadioGroup, GlFormRadio } from '@gitlab/ui';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import Vuex from 'vuex';
import { MOCK_QUERY } from 'jest/search/mock_data';
import RadioFilter from '~/search/sidebar/components/radio_filter.vue';
-import { stateFilterData } from '~/search/sidebar/constants/state_filter_data';
import { confidentialFilterData } from '~/search/sidebar/constants/confidential_filter_data';
+import { stateFilterData } from '~/search/sidebar/constants/state_filter_data';
const localVue = createLocalVue();
localVue.use(Vuex);
diff --git a/spec/frontend/search/sidebar/components/status_filter_spec.js b/spec/frontend/search/sidebar/components/status_filter_spec.js
index 21fc663397e..08ce57b206b 100644
--- a/spec/frontend/search/sidebar/components/status_filter_spec.js
+++ b/spec/frontend/search/sidebar/components/status_filter_spec.js
@@ -1,8 +1,8 @@
-import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
+import Vuex from 'vuex';
import { MOCK_QUERY } from 'jest/search/mock_data';
-import StatusFilter from '~/search/sidebar/components/status_filter.vue';
import RadioFilter from '~/search/sidebar/components/radio_filter.vue';
+import StatusFilter from '~/search/sidebar/components/status_filter.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
diff --git a/spec/frontend/search/sort/components/app_spec.js b/spec/frontend/search/sort/components/app_spec.js
new file mode 100644
index 00000000000..5806d6b51d2
--- /dev/null
+++ b/spec/frontend/search/sort/components/app_spec.js
@@ -0,0 +1,168 @@
+import { GlButtonGroup, GlButton, GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import Vuex from 'vuex';
+import { MOCK_QUERY, MOCK_SORT_OPTIONS } from 'jest/search/mock_data';
+import GlobalSearchSort from '~/search/sort/components/app.vue';
+import { SORT_DIRECTION_UI } from '~/search/sort/constants';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('GlobalSearchSort', () => {
+ let wrapper;
+
+ const actionSpies = {
+ setQuery: jest.fn(),
+ applyQuery: jest.fn(),
+ };
+
+ const defaultProps = {
+ searchSortOptions: MOCK_SORT_OPTIONS,
+ };
+
+ const createComponent = (initialState, props) => {
+ const store = new Vuex.Store({
+ state: {
+ query: MOCK_QUERY,
+ ...initialState,
+ },
+ actions: actionSpies,
+ });
+
+ wrapper = shallowMount(GlobalSearchSort, {
+ localVue,
+ store,
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findSortButtonGroup = () => wrapper.find(GlButtonGroup);
+ const findSortDropdown = () => wrapper.find(GlDropdown);
+ const findSortDirectionButton = () => wrapper.find(GlButton);
+ const findDropdownItems = () => findSortDropdown().findAll(GlDropdownItem);
+ const findDropdownItemsText = () => findDropdownItems().wrappers.map((w) => w.text());
+
+ describe('template', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders Sort Button Group', () => {
+ expect(findSortButtonGroup().exists()).toBe(true);
+ });
+
+ it('renders Sort Dropdown', () => {
+ expect(findSortDropdown().exists()).toBe(true);
+ });
+
+ it('renders Sort Direction Button', () => {
+ expect(findSortDirectionButton().exists()).toBe(true);
+ });
+ });
+
+ describe('Sort Dropdown Items', () => {
+ describe('renders', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('an instance for each namespace', () => {
+ expect(findDropdownItemsText()).toStrictEqual(
+ MOCK_SORT_OPTIONS.map((option) => option.title),
+ );
+ });
+ });
+
+ describe.each`
+ sortQuery | value
+ ${null} | ${MOCK_SORT_OPTIONS[0].title}
+ ${'asdf'} | ${MOCK_SORT_OPTIONS[0].title}
+ ${MOCK_SORT_OPTIONS[0].sortParam} | ${MOCK_SORT_OPTIONS[0].title}
+ ${MOCK_SORT_OPTIONS[1].sortParam.desc} | ${MOCK_SORT_OPTIONS[1].title}
+ ${MOCK_SORT_OPTIONS[1].sortParam.asc} | ${MOCK_SORT_OPTIONS[1].title}
+ `('selected', ({ sortQuery, value }) => {
+ describe(`when sort option is ${sortQuery}`, () => {
+ beforeEach(() => {
+ createComponent({ query: { sort: sortQuery } });
+ });
+
+ it('is set correctly', () => {
+ expect(findSortDropdown().attributes('text')).toBe(value);
+ });
+ });
+ });
+ });
+
+ describe.each`
+ description | sortQuery | sortUi | disabled
+ ${'non-sortable'} | ${MOCK_SORT_OPTIONS[0].sortParam} | ${SORT_DIRECTION_UI.disabled} | ${'true'}
+ ${'descending sortable'} | ${MOCK_SORT_OPTIONS[1].sortParam.desc} | ${SORT_DIRECTION_UI.desc} | ${undefined}
+ ${'ascending sortable'} | ${MOCK_SORT_OPTIONS[1].sortParam.asc} | ${SORT_DIRECTION_UI.asc} | ${undefined}
+ `('Sort Direction Button', ({ description, sortQuery, sortUi, disabled }) => {
+ describe(`when sort option is ${description}`, () => {
+ beforeEach(() => {
+ createComponent({ query: { sort: sortQuery } });
+ });
+
+ it('sets the UI correctly', () => {
+ expect(findSortDirectionButton().attributes('disabled')).toBe(disabled);
+ expect(findSortDirectionButton().attributes('title')).toBe(sortUi.tooltip);
+ expect(findSortDirectionButton().attributes('icon')).toBe(sortUi.icon);
+ });
+ });
+ });
+
+ describe('actions', () => {
+ describe.each`
+ description | index | value
+ ${'non-sortable'} | ${0} | ${MOCK_SORT_OPTIONS[0].sortParam}
+ ${'sortable'} | ${1} | ${MOCK_SORT_OPTIONS[1].sortParam.desc}
+ `('handleSortChange', ({ description, index, value }) => {
+ describe(`when clicking a ${description} option`, () => {
+ beforeEach(() => {
+ createComponent();
+ findDropdownItems().at(index).vm.$emit('click');
+ });
+
+ it('calls setQuery and applyQuery correctly', () => {
+ expect(actionSpies.setQuery).toHaveBeenCalledTimes(1);
+ expect(actionSpies.applyQuery).toHaveBeenCalledTimes(1);
+ expect(actionSpies.setQuery).toHaveBeenCalledWith(expect.any(Object), {
+ key: 'sort',
+ value,
+ });
+ });
+ });
+ });
+
+ describe.each`
+ description | sortQuery | value
+ ${'descending'} | ${MOCK_SORT_OPTIONS[1].sortParam.desc} | ${MOCK_SORT_OPTIONS[1].sortParam.asc}
+ ${'ascending'} | ${MOCK_SORT_OPTIONS[1].sortParam.asc} | ${MOCK_SORT_OPTIONS[1].sortParam.desc}
+ `('handleSortDirectionChange', ({ description, sortQuery, value }) => {
+ describe(`when toggling a ${description} option`, () => {
+ beforeEach(() => {
+ createComponent({ query: { sort: sortQuery } });
+ findSortDirectionButton().vm.$emit('click');
+ });
+
+ it('calls setQuery and applyQuery correctly', () => {
+ expect(actionSpies.setQuery).toHaveBeenCalledTimes(1);
+ expect(actionSpies.applyQuery).toHaveBeenCalledTimes(1);
+ expect(actionSpies.setQuery).toHaveBeenCalledWith(expect.any(Object), {
+ key: 'sort',
+ value,
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/search/store/actions_spec.js b/spec/frontend/search/store/actions_spec.js
index e4536a3e136..ab622c53387 100644
--- a/spec/frontend/search/store/actions_spec.js
+++ b/spec/frontend/search/store/actions_spec.js
@@ -1,12 +1,12 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
+import createFlash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import * as urlUtils from '~/lib/utils/url_utility';
import * as actions from '~/search/store/actions';
import * as types from '~/search/store/mutation_types';
-import * as urlUtils from '~/lib/utils/url_utility';
import createState from '~/search/store/state';
-import axios from '~/lib/utils/axios_utils';
-import createFlash from '~/flash';
import { MOCK_QUERY, MOCK_GROUPS, MOCK_PROJECT, MOCK_PROJECTS } from '../mock_data';
jest.mock('~/flash');
diff --git a/spec/frontend/search/store/mutations_spec.js b/spec/frontend/search/store/mutations_spec.js
index 560ed66263b..df94ba40ff2 100644
--- a/spec/frontend/search/store/mutations_spec.js
+++ b/spec/frontend/search/store/mutations_spec.js
@@ -1,6 +1,6 @@
+import * as types from '~/search/store/mutation_types';
import mutations from '~/search/store/mutations';
import createState from '~/search/store/state';
-import * as types from '~/search/store/mutation_types';
import { MOCK_QUERY, MOCK_GROUPS, MOCK_PROJECTS } from '../mock_data';
describe('Global Search Store Mutations', () => {
diff --git a/spec/frontend/search/topbar/components/app_spec.js b/spec/frontend/search/topbar/components/app_spec.js
new file mode 100644
index 00000000000..fb953f2ed1b
--- /dev/null
+++ b/spec/frontend/search/topbar/components/app_spec.js
@@ -0,0 +1,113 @@
+import { GlForm, GlSearchBoxByType, GlButton } from '@gitlab/ui';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import Vuex from 'vuex';
+import { MOCK_QUERY } from 'jest/search/mock_data';
+import GlobalSearchTopbar from '~/search/topbar/components/app.vue';
+import GroupFilter from '~/search/topbar/components/group_filter.vue';
+import ProjectFilter from '~/search/topbar/components/project_filter.vue';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('GlobalSearchTopbar', () => {
+ let wrapper;
+
+ const actionSpies = {
+ applyQuery: jest.fn(),
+ setQuery: jest.fn(),
+ };
+
+ const createComponent = (initialState) => {
+ const store = new Vuex.Store({
+ state: {
+ query: MOCK_QUERY,
+ ...initialState,
+ },
+ actions: actionSpies,
+ });
+
+ wrapper = shallowMount(GlobalSearchTopbar, {
+ localVue,
+ store,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findTopbarForm = () => wrapper.find(GlForm);
+ const findGlSearchBox = () => wrapper.find(GlSearchBoxByType);
+ const findGroupFilter = () => wrapper.find(GroupFilter);
+ const findProjectFilter = () => wrapper.find(ProjectFilter);
+ const findSearchButton = () => wrapper.find(GlButton);
+
+ describe('template', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders Topbar Form always', () => {
+ expect(findTopbarForm().exists()).toBe(true);
+ });
+
+ describe('Search box', () => {
+ it('renders always', () => {
+ expect(findGlSearchBox().exists()).toBe(true);
+ });
+
+ describe('onSearch', () => {
+ const testSearch = 'test search';
+
+ beforeEach(() => {
+ findGlSearchBox().vm.$emit('input', testSearch);
+ });
+
+ it('calls setQuery when input event is fired from GlSearchBoxByType', () => {
+ expect(actionSpies.setQuery).toHaveBeenCalledWith(expect.any(Object), {
+ key: 'search',
+ value: testSearch,
+ });
+ });
+ });
+ });
+
+ describe.each`
+ snippets | showFilters
+ ${null} | ${true}
+ ${{ query: { snippets: '' } }} | ${true}
+ ${{ query: { snippets: false } }} | ${true}
+ ${{ query: { snippets: true } }} | ${false}
+ ${{ query: { snippets: 'false' } }} | ${true}
+ ${{ query: { snippets: 'true' } }} | ${false}
+ `('topbar filters', ({ snippets, showFilters }) => {
+ beforeEach(() => {
+ createComponent(snippets);
+ });
+
+ it(`does${showFilters ? '' : ' not'} render when snippets is ${JSON.stringify(
+ snippets,
+ )}`, () => {
+ expect(findGroupFilter().exists()).toBe(showFilters);
+ expect(findProjectFilter().exists()).toBe(showFilters);
+ });
+ });
+
+ it('renders SearchButton always', () => {
+ expect(findSearchButton().exists()).toBe(true);
+ });
+ });
+
+ describe('actions', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('clicking SearchButton calls applyQuery', () => {
+ findTopbarForm().vm.$emit('submit', { preventDefault: () => {} });
+
+ expect(actionSpies.applyQuery).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/search/topbar/components/group_filter_spec.js b/spec/frontend/search/topbar/components/group_filter_spec.js
index 017808d576e..15b46f9c058 100644
--- a/spec/frontend/search/topbar/components/group_filter_spec.js
+++ b/spec/frontend/search/topbar/components/group_filter_spec.js
@@ -1,5 +1,5 @@
-import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
+import Vuex from 'vuex';
import { MOCK_GROUP, MOCK_QUERY } from 'jest/search/mock_data';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import GroupFilter from '~/search/topbar/components/group_filter.vue';
diff --git a/spec/frontend/search/topbar/components/project_filter_spec.js b/spec/frontend/search/topbar/components/project_filter_spec.js
index c1fc61d7e89..3bd0769b34a 100644
--- a/spec/frontend/search/topbar/components/project_filter_spec.js
+++ b/spec/frontend/search/topbar/components/project_filter_spec.js
@@ -1,5 +1,5 @@
-import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
+import Vuex from 'vuex';
import { MOCK_PROJECT, MOCK_QUERY } from 'jest/search/mock_data';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import ProjectFilter from '~/search/topbar/components/project_filter.vue';
@@ -99,7 +99,7 @@ describe('ProjectFilter', () => {
it('calls setUrlParams with project id, group id, then calls visitUrl', () => {
expect(setUrlParams).toHaveBeenCalledWith({
- [GROUP_DATA.queryParam]: MOCK_PROJECT.namespace_id,
+ [GROUP_DATA.queryParam]: MOCK_PROJECT.namespace.id,
[PROJECT_DATA.queryParam]: MOCK_PROJECT.id,
});
expect(visitUrl).toHaveBeenCalled();
diff --git a/spec/frontend/search/topbar/components/searchable_dropdown_spec.js b/spec/frontend/search/topbar/components/searchable_dropdown_spec.js
index 86e29571d0f..5de948592d4 100644
--- a/spec/frontend/search/topbar/components/searchable_dropdown_spec.js
+++ b/spec/frontend/search/topbar/components/searchable_dropdown_spec.js
@@ -1,6 +1,6 @@
-import Vuex from 'vuex';
-import { createLocalVue, shallowMount, mount } from '@vue/test-utils';
import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlSkeletonLoader } from '@gitlab/ui';
+import { createLocalVue, shallowMount, mount } from '@vue/test-utils';
+import Vuex from 'vuex';
import { MOCK_GROUPS, MOCK_GROUP, MOCK_QUERY } from 'jest/search/mock_data';
import SearchableDropdown from '~/search/topbar/components/searchable_dropdown.vue';
import { ANY_OPTION, GROUP_DATA } from '~/search/topbar/constants';
diff --git a/spec/frontend/search_autocomplete_spec.js b/spec/frontend/search_autocomplete_spec.js
index e844bc96e05..a9fbe0fe552 100644
--- a/spec/frontend/search_autocomplete_spec.js
+++ b/spec/frontend/search_autocomplete_spec.js
@@ -1,11 +1,10 @@
/* eslint-disable no-unused-expressions, consistent-return, no-param-reassign, default-case, no-return-assign */
-
-import $ from 'jquery';
import AxiosMockAdapter from 'axios-mock-adapter';
+import $ from 'jquery';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+import axios from '~/lib/utils/axios_utils';
import initSearchAutocomplete from '~/search_autocomplete';
import '~/lib/utils/common_utils';
-import axios from '~/lib/utils/axios_utils';
describe('Search autocomplete dropdown', () => {
let widget = null;
diff --git a/spec/frontend/search_settings/components/search_settings_spec.js b/spec/frontend/search_settings/components/search_settings_spec.js
index b80f9b15abf..173936e1ce3 100644
--- a/spec/frontend/search_settings/components/search_settings_spec.js
+++ b/spec/frontend/search_settings/components/search_settings_spec.js
@@ -2,6 +2,7 @@ import { GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import SearchSettings from '~/search_settings/components/search_settings.vue';
import { HIGHLIGHT_CLASS, HIDE_CLASS } from '~/search_settings/constants';
+import { isExpanded, expandSection, closeSection } from '~/settings_panels';
describe('search_settings/components/search_settings.vue', () => {
const ROOT_ID = 'content-body';
@@ -9,6 +10,8 @@ describe('search_settings/components/search_settings.vue', () => {
const SEARCH_TERM = 'Delete project';
const GENERAL_SETTINGS_ID = 'js-general-settings';
const ADVANCED_SETTINGS_ID = 'js-advanced-settings';
+ const EXTRA_SETTINGS_ID = 'js-extra-settings';
+
let wrapper;
const buildWrapper = () => {
@@ -16,10 +19,15 @@ describe('search_settings/components/search_settings.vue', () => {
propsData: {
searchRoot: document.querySelector(`#${ROOT_ID}`),
sectionSelector: SECTION_SELECTOR,
+ isExpandedFn: isExpanded,
+ },
+ // Add real listeners so we can simplify and strengthen some tests.
+ listeners: {
+ expand: expandSection,
+ collapse: closeSection,
},
});
};
-
const sections = () => Array.from(document.querySelectorAll(SECTION_SELECTOR));
const sectionsCount = () => sections().length;
const visibleSectionsCount = () =>
@@ -39,7 +47,10 @@ describe('search_settings/components/search_settings.vue', () => {
<section id="${GENERAL_SETTINGS_ID}" class="settings">
<span>General</span>
</section>
- <section id="${ADVANCED_SETTINGS_ID}" class="settings">
+ <section id="${ADVANCED_SETTINGS_ID}" class="settings expanded">
+ <span>Advanced</span>
+ </section>
+ <section id="${EXTRA_SETTINGS_ID}" class="settings">
<span>${SEARCH_TERM}</span>
</section>
</div>
@@ -52,17 +63,6 @@ describe('search_settings/components/search_settings.vue', () => {
wrapper.destroy();
});
- it('expands first section and collapses the rest', () => {
- clearSearch();
-
- const [firstSection, ...otherSections] = sections();
-
- expect(wrapper.emitted()).toEqual({
- expand: [[firstSection]],
- collapse: otherSections.map((x) => [x]),
- });
- });
-
it('hides sections that do not match the search term', () => {
const hiddenSection = document.querySelector(`#${GENERAL_SETTINGS_ID}`);
search(SEARCH_TERM);
@@ -72,12 +72,11 @@ describe('search_settings/components/search_settings.vue', () => {
});
it('expands section that matches the search term', () => {
- const section = document.querySelector(`#${ADVANCED_SETTINGS_ID}`);
+ const section = document.querySelector(`#${EXTRA_SETTINGS_ID}`);
search(SEARCH_TERM);
- // Last called because expand is always called once to reset the page state
- expect(wrapper.emitted().expand[1][0]).toBe(section);
+ expect(wrapper.emitted('expand')).toEqual([[section]]);
});
it('highlight elements that match the search term', () => {
@@ -86,21 +85,64 @@ describe('search_settings/components/search_settings.vue', () => {
expect(highlightedElementsCount()).toBe(1);
});
- describe('when search term is cleared', () => {
- beforeEach(() => {
- search(SEARCH_TERM);
- });
-
- it('displays all sections', () => {
- expect(visibleSectionsCount()).toBe(1);
- clearSearch();
- expect(visibleSectionsCount()).toBe(sectionsCount());
+ describe('default', () => {
+ it('test setup starts with expansion state', () => {
+ expect(sections().map(isExpanded)).toEqual([false, true, false]);
});
- it('removes the highlight from all elements', () => {
- expect(highlightedElementsCount()).toBe(1);
- clearSearch();
- expect(highlightedElementsCount()).toBe(0);
+ describe('when searched and cleared', () => {
+ beforeEach(() => {
+ search('Test');
+ clearSearch();
+ });
+
+ it('displays all sections', () => {
+ expect(visibleSectionsCount()).toBe(sectionsCount());
+ });
+
+ it('removes the highlight from all elements', () => {
+ expect(highlightedElementsCount()).toBe(0);
+ });
+
+ it('should preserve original expansion state', () => {
+ expect(sections().map(isExpanded)).toEqual([false, true, false]);
+ });
+
+ it('should preserve state by emitting events', () => {
+ const [first, mid, last] = sections();
+
+ expect(wrapper.emitted()).toEqual({
+ expand: [[mid]],
+ collapse: [[first], [last]],
+ });
+ });
+
+ describe('after multiple searches and clear', () => {
+ beforeEach(() => {
+ search('Test');
+ search(SEARCH_TERM);
+ clearSearch();
+ });
+
+ it('should preserve last expansion state', () => {
+ expect(sections().map(isExpanded)).toEqual([false, true, false]);
+ });
+ });
+
+ describe('after user expands and collapses, search, and clear', () => {
+ beforeEach(() => {
+ const [first, mid] = sections();
+ closeSection(mid);
+ expandSection(first);
+
+ search(SEARCH_TERM);
+ clearSearch();
+ });
+
+ it('should preserve last expansion state', () => {
+ expect(sections().map(isExpanded)).toEqual([true, false, false]);
+ });
+ });
});
});
});
diff --git a/spec/frontend/search_settings/index_spec.js b/spec/frontend/search_settings/index_spec.js
index 122ee1251bb..1d56d054eea 100644
--- a/spec/frontend/search_settings/index_spec.js
+++ b/spec/frontend/search_settings/index_spec.js
@@ -1,36 +1,25 @@
-import $ from 'jquery';
-import { setHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import initSearch from '~/search_settings';
-import { expandSection, closeSection } from '~/settings_panels';
+import mount from '~/search_settings/mount';
-jest.mock('~/settings_panels');
-
-describe('search_settings/index', () => {
- let app;
-
- beforeEach(() => {
- const el = document.createElement('div');
-
- setHTMLFixture('<div id="content-body"></div>');
-
- app = initSearch({ el });
- });
+jest.mock('~/search_settings/mount');
+describe('~/search_settings', () => {
afterEach(() => {
- app.$destroy();
+ resetHTMLFixture();
});
- it('calls settings_panel.onExpand when expand event is emitted', () => {
- const section = { name: 'section' };
- app.$refs.searchSettings.$emit('expand', section);
+ it('initializes search settings when js-search-settings-app is available', async () => {
+ setHTMLFixture('<div class="js-search-settings-app"></div>');
+
+ await initSearch();
- expect(expandSection).toHaveBeenCalledWith($(section));
+ expect(mount).toHaveBeenCalled();
});
- it('calls settings_panel.closeSection when collapse event is emitted', () => {
- const section = { name: 'section' };
- app.$refs.searchSettings.$emit('collapse', section);
+ it('does not initialize search settings when js-search-settings-app is unavailable', async () => {
+ await initSearch();
- expect(closeSection).toHaveBeenCalledWith($(section));
+ expect(mount).not.toHaveBeenCalled();
});
});
diff --git a/spec/frontend/search_settings/mount_spec.js b/spec/frontend/search_settings/mount_spec.js
new file mode 100644
index 00000000000..8c141c4704e
--- /dev/null
+++ b/spec/frontend/search_settings/mount_spec.js
@@ -0,0 +1,35 @@
+import { setHTMLFixture } from 'helpers/fixtures';
+import mount from '~/search_settings/mount';
+import { expandSection, closeSection } from '~/settings_panels';
+
+jest.mock('~/settings_panels');
+
+describe('search_settings/mount', () => {
+ let app;
+
+ beforeEach(() => {
+ const el = document.createElement('div');
+
+ setHTMLFixture('<div id="content-body"></div>');
+
+ app = mount({ el });
+ });
+
+ afterEach(() => {
+ app.$destroy();
+ });
+
+ it('calls settings_panel.onExpand when expand event is emitted', () => {
+ const section = { name: 'section' };
+ app.$refs.searchSettings.$emit('expand', section);
+
+ expect(expandSection).toHaveBeenCalledWith(section);
+ });
+
+ it('calls settings_panel.closeSection when collapse event is emitted', () => {
+ const section = { name: 'section' };
+ app.$refs.searchSettings.$emit('collapse', section);
+
+ expect(closeSection).toHaveBeenCalledWith(section);
+ });
+});
diff --git a/spec/frontend/search_spec.js b/spec/frontend/search_spec.js
deleted file mode 100644
index d234a7fccb9..00000000000
--- a/spec/frontend/search_spec.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import setHighlightClass from 'ee_else_ce/search/highlight_blob_search_result';
-import Search from '~/pages/search/show/search';
-
-jest.mock('~/api');
-jest.mock('ee_else_ce/search/highlight_blob_search_result');
-
-describe('Search', () => {
- const fixturePath = 'search/show.html';
-
- preloadFixtures(fixturePath);
-
- describe('constructor side effects', () => {
- afterEach(() => {
- jest.restoreAllMocks();
- });
-
- it('highlights lines with search terms in blob search results', () => {
- new Search(); // eslint-disable-line no-new
-
- expect(setHighlightClass).toHaveBeenCalled();
- });
- });
-});
diff --git a/spec/frontend/security_configuration/app_spec.js b/spec/frontend/security_configuration/app_spec.js
new file mode 100644
index 00000000000..11d481fb210
--- /dev/null
+++ b/spec/frontend/security_configuration/app_spec.js
@@ -0,0 +1,27 @@
+import { shallowMount } from '@vue/test-utils';
+import App from '~/security_configuration/components/app.vue';
+import ConfigurationTable from '~/security_configuration/components/configuration_table.vue';
+
+describe('App Component', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(App, {});
+ };
+ const findConfigurationTable = () => wrapper.findComponent(ConfigurationTable);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders correct primary & Secondary Heading', () => {
+ createComponent();
+ expect(wrapper.text()).toContain('Security Configuration');
+ expect(wrapper.text()).toContain('Testing & Compliance');
+ });
+
+ it('renders ConfigurationTable Component', () => {
+ createComponent();
+ expect(findConfigurationTable().exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/security_configuration/configuration_table_spec.js b/spec/frontend/security_configuration/configuration_table_spec.js
new file mode 100644
index 00000000000..49f9a7a3ea8
--- /dev/null
+++ b/spec/frontend/security_configuration/configuration_table_spec.js
@@ -0,0 +1,48 @@
+import { mount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import ConfigurationTable from '~/security_configuration/components/configuration_table.vue';
+import { features, UPGRADE_CTA } from '~/security_configuration/components/features_constants';
+
+import {
+ REPORT_TYPE_SAST,
+ REPORT_TYPE_DAST,
+ REPORT_TYPE_DEPENDENCY_SCANNING,
+ REPORT_TYPE_CONTAINER_SCANNING,
+ REPORT_TYPE_COVERAGE_FUZZING,
+ REPORT_TYPE_LICENSE_COMPLIANCE,
+} from '~/vue_shared/security_reports/constants';
+
+describe('Configuration Table Component', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = extendedWrapper(mount(ConfigurationTable, {}));
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it.each(features)('should match strings', (feature) => {
+ expect(wrapper.text()).toContain(feature.name);
+ expect(wrapper.text()).toContain(feature.description);
+
+ if (feature.type === REPORT_TYPE_SAST) {
+ expect(wrapper.findByTestId(feature.type).text()).toBe('Configure via Merge Request');
+ } else if (
+ [
+ REPORT_TYPE_DAST,
+ REPORT_TYPE_DEPENDENCY_SCANNING,
+ REPORT_TYPE_CONTAINER_SCANNING,
+ REPORT_TYPE_COVERAGE_FUZZING,
+ REPORT_TYPE_LICENSE_COMPLIANCE,
+ ].includes(feature.type)
+ ) {
+ expect(wrapper.findByTestId(feature.type).text()).toMatchInterpolatedText(UPGRADE_CTA);
+ }
+ });
+});
diff --git a/spec/frontend/security_configuration/manage_sast_spec.js b/spec/frontend/security_configuration/manage_sast_spec.js
new file mode 100644
index 00000000000..7c76f19ddb4
--- /dev/null
+++ b/spec/frontend/security_configuration/manage_sast_spec.js
@@ -0,0 +1,136 @@
+import { GlButton } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { redirectTo } from '~/lib/utils/url_utility';
+import ManageSast from '~/security_configuration/components/manage_sast.vue';
+import configureSastMutation from '~/security_configuration/graphql/configure_sast.mutation.graphql';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ redirectTo: jest.fn(),
+}));
+
+Vue.use(VueApollo);
+
+describe('Manage Sast Component', () => {
+ let wrapper;
+
+ const findButton = () => wrapper.findComponent(GlButton);
+ const successHandler = async () => {
+ return {
+ data: {
+ configureSast: {
+ successPath: 'testSuccessPath',
+ errors: [],
+ __typename: 'ConfigureSastPayload',
+ },
+ },
+ };
+ };
+
+ const noSuccessPathHandler = async () => {
+ return {
+ data: {
+ configureSast: {
+ successPath: '',
+ errors: [],
+ __typename: 'ConfigureSastPayload',
+ },
+ },
+ };
+ };
+
+ const errorHandler = async () => {
+ return {
+ data: {
+ configureSast: {
+ successPath: 'testSuccessPath',
+ errors: ['foo'],
+ __typename: 'ConfigureSastPayload',
+ },
+ },
+ };
+ };
+
+ const pendingHandler = () => new Promise(() => {});
+
+ function createMockApolloProvider(handler) {
+ const requestHandlers = [[configureSastMutation, handler]];
+
+ return createMockApollo(requestHandlers);
+ }
+
+ function createComponent(options = {}) {
+ const { mockApollo } = options;
+ wrapper = extendedWrapper(
+ mount(ManageSast, {
+ apolloProvider: mockApollo,
+ }),
+ );
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('should render Button with correct text', () => {
+ createComponent();
+ expect(findButton().text()).toContain('Configure via Merge Request');
+ });
+
+ describe('given a successful response', () => {
+ beforeEach(() => {
+ const mockApollo = createMockApolloProvider(successHandler);
+ createComponent({ mockApollo });
+ });
+
+ it('should call redirect helper with correct value', async () => {
+ await wrapper.trigger('click');
+ await waitForPromises();
+ expect(redirectTo).toHaveBeenCalledTimes(1);
+ expect(redirectTo).toHaveBeenCalledWith('testSuccessPath');
+ // This is done for UX reasons. If the loading prop is set to false
+ // on success, then there's a period where the button is clickable
+ // again. Instead, we want the button to display a loading indicator
+ // for the remainder of the lifetime of the page (i.e., until the
+ // browser can start painting the new page it's been redirected to).
+ expect(findButton().props().loading).toBe(true);
+ });
+ });
+
+ describe('given a pending response', () => {
+ beforeEach(() => {
+ const mockApollo = createMockApolloProvider(pendingHandler);
+ createComponent({ mockApollo });
+ });
+
+ it('renders spinner correctly', async () => {
+ expect(findButton().props('loading')).toBe(false);
+ await wrapper.trigger('click');
+ await waitForPromises();
+ expect(findButton().props('loading')).toBe(true);
+ });
+ });
+
+ describe.each`
+ handler | message
+ ${noSuccessPathHandler} | ${'SAST merge request creation mutation failed'}
+ ${errorHandler} | ${'foo'}
+ `('given an error response', ({ handler, message }) => {
+ beforeEach(() => {
+ const mockApollo = createMockApolloProvider(handler);
+ createComponent({ mockApollo });
+ });
+
+ it('should catch and emit error', async () => {
+ await wrapper.trigger('click');
+ await waitForPromises();
+ expect(wrapper.emitted('error')).toEqual([[message]]);
+ expect(findButton().props('loading')).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/security_configuration/upgrade_spec.js b/spec/frontend/security_configuration/upgrade_spec.js
new file mode 100644
index 00000000000..0ab1108b265
--- /dev/null
+++ b/spec/frontend/security_configuration/upgrade_spec.js
@@ -0,0 +1,29 @@
+import { mount } from '@vue/test-utils';
+import { UPGRADE_CTA } from '~/security_configuration/components/features_constants';
+import Upgrade from '~/security_configuration/components/upgrade.vue';
+
+let wrapper;
+const createComponent = () => {
+ wrapper = mount(Upgrade, {});
+};
+
+beforeEach(() => {
+ createComponent();
+});
+
+afterEach(() => {
+ wrapper.destroy();
+});
+
+describe('Upgrade component', () => {
+ it('renders correct text in link', () => {
+ expect(wrapper.text()).toMatchInterpolatedText(UPGRADE_CTA);
+ });
+
+ it('renders link with correct attributes', () => {
+ expect(wrapper.find('a').attributes()).toMatchObject({
+ href: 'https://about.gitlab.com/pricing/',
+ target: '_blank',
+ });
+ });
+});
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 dfa961c5115..5f5934305c6 100644
--- a/spec/frontend/self_monitor/components/self_monitor_form_spec.js
+++ b/spec/frontend/self_monitor/components/self_monitor_form_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
import SelfMonitor from '~/self_monitor/components/self_monitor_form.vue';
import { createStore } from '~/self_monitor/store';
diff --git a/spec/frontend/sentry/index_spec.js b/spec/frontend/sentry/index_spec.js
index 82b6c445d96..13b9b9e909c 100644
--- a/spec/frontend/sentry/index_spec.js
+++ b/spec/frontend/sentry/index_spec.js
@@ -1,5 +1,5 @@
-import SentryConfig from '~/sentry/sentry_config';
import index from '~/sentry/index';
+import SentryConfig from '~/sentry/sentry_config';
describe('SentryConfig options', () => {
const dsn = 'https://123@sentry.gitlab.test/123';
diff --git a/spec/frontend/sentry/sentry_config_spec.js b/spec/frontend/sentry/sentry_config_spec.js
index 5ee261f480a..f7102f9b2f9 100644
--- a/spec/frontend/sentry/sentry_config_spec.js
+++ b/spec/frontend/sentry/sentry_config_spec.js
@@ -1,5 +1,5 @@
-import * as Sentry from '~/sentry/wrapper';
import SentryConfig from '~/sentry/sentry_config';
+import * as Sentry from '~/sentry/wrapper';
describe('SentryConfig', () => {
describe('IGNORE_ERRORS', () => {
diff --git a/spec/frontend/sentry_error_stack_trace/components/sentry_error_stack_trace_spec.js b/spec/frontend/sentry_error_stack_trace/components/sentry_error_stack_trace_spec.js
index 135a3844e78..772d6903052 100644
--- a/spec/frontend/sentry_error_stack_trace/components/sentry_error_stack_trace_spec.js
+++ b/spec/frontend/sentry_error_stack_trace/components/sentry_error_stack_trace_spec.js
@@ -1,6 +1,6 @@
+import { GlLoadingIcon } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
-import { GlLoadingIcon } from '@gitlab/ui';
import Stacktrace from '~/error_tracking/components/stacktrace.vue';
import SentryErrorStackTrace from '~/sentry_error_stack_trace/components/sentry_error_stack_trace.vue';
diff --git a/spec/frontend/serverless/components/empty_state_spec.js b/spec/frontend/serverless/components/empty_state_spec.js
index daa1576a4ec..d63882c2a6d 100644
--- a/spec/frontend/serverless/components/empty_state_spec.js
+++ b/spec/frontend/serverless/components/empty_state_spec.js
@@ -1,7 +1,7 @@
import { GlEmptyState, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { createStore } from '~/serverless/store';
import EmptyStateComponent from '~/serverless/components/empty_state.vue';
+import { createStore } from '~/serverless/store';
describe('EmptyStateComponent', () => {
let wrapper;
diff --git a/spec/frontend/serverless/components/environment_row_spec.js b/spec/frontend/serverless/components/environment_row_spec.js
index a59b4fdbb7b..944283136d0 100644
--- a/spec/frontend/serverless/components/environment_row_spec.js
+++ b/spec/frontend/serverless/components/environment_row_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
import environmentRowComponent from '~/serverless/components/environment_row.vue';
-import { mockServerlessFunctions, mockServerlessFunctionsDiffEnv } from '../mock_data';
import { translate } from '~/serverless/utils';
+import { mockServerlessFunctions, mockServerlessFunctionsDiffEnv } from '../mock_data';
const createComponent = (env, envName) =>
shallowMount(environmentRowComponent, {
diff --git a/spec/frontend/serverless/components/function_details_spec.js b/spec/frontend/serverless/components/function_details_spec.js
index 248dd418941..d2b8de71e01 100644
--- a/spec/frontend/serverless/components/function_details_spec.js
+++ b/spec/frontend/serverless/components/function_details_spec.js
@@ -1,6 +1,6 @@
+import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
import functionDetailsComponent from '~/serverless/components/function_details.vue';
import { createStore } from '~/serverless/store';
diff --git a/spec/frontend/serverless/components/functions_spec.js b/spec/frontend/serverless/components/functions_spec.js
index 0fca027fe56..01dd512c5d3 100644
--- a/spec/frontend/serverless/components/functions_spec.js
+++ b/spec/frontend/serverless/components/functions_spec.js
@@ -1,13 +1,13 @@
-import Vuex from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
-import AxiosMockAdapter from 'axios-mock-adapter';
import { createLocalVue, shallowMount } from '@vue/test-utils';
+import AxiosMockAdapter from 'axios-mock-adapter';
+import Vuex from 'vuex';
import { TEST_HOST } from 'helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
-import functionsComponent from '~/serverless/components/functions.vue';
-import { createStore } from '~/serverless/store';
import EmptyState from '~/serverless/components/empty_state.vue';
import EnvironmentRow from '~/serverless/components/environment_row.vue';
+import functionsComponent from '~/serverless/components/functions.vue';
+import { createStore } from '~/serverless/store';
import { mockServerlessFunctions } from '../mock_data';
describe('functionsComponent', () => {
diff --git a/spec/frontend/serverless/components/missing_prometheus_spec.js b/spec/frontend/serverless/components/missing_prometheus_spec.js
index ffdb1f13111..d5b187452c6 100644
--- a/spec/frontend/serverless/components/missing_prometheus_spec.js
+++ b/spec/frontend/serverless/components/missing_prometheus_spec.js
@@ -1,7 +1,7 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { createStore } from '~/serverless/store';
import missingPrometheusComponent from '~/serverless/components/missing_prometheus.vue';
+import { createStore } from '~/serverless/store';
describe('missingPrometheusComponent', () => {
let wrapper;
diff --git a/spec/frontend/serverless/components/url_spec.js b/spec/frontend/serverless/components/url_spec.js
index c43933e5b94..8c839577aa0 100644
--- a/spec/frontend/serverless/components/url_spec.js
+++ b/spec/frontend/serverless/components/url_spec.js
@@ -1,5 +1,5 @@
-import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import urlComponent from '~/serverless/components/url.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
diff --git a/spec/frontend/serverless/store/actions_spec.js b/spec/frontend/serverless/store/actions_spec.js
index 32e30a57d4b..61b9bd121af 100644
--- a/spec/frontend/serverless/store/actions_spec.js
+++ b/spec/frontend/serverless/store/actions_spec.js
@@ -1,9 +1,9 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
+import axios from '~/lib/utils/axios_utils';
import statusCodes from '~/lib/utils/http_status';
import { fetchFunctions, fetchMetrics } from '~/serverless/store/actions';
import { mockServerlessFunctions, mockMetrics } from '../mock_data';
-import axios from '~/lib/utils/axios_utils';
import { adjustMetricQuery } from '../utils';
describe('ServerlessActions', () => {
diff --git a/spec/frontend/serverless/store/getters_spec.js b/spec/frontend/serverless/store/getters_spec.js
index 92853fda37c..e1942bd2759 100644
--- a/spec/frontend/serverless/store/getters_spec.js
+++ b/spec/frontend/serverless/store/getters_spec.js
@@ -1,5 +1,5 @@
-import serverlessState from '~/serverless/store/state';
import * as getters from '~/serverless/store/getters';
+import serverlessState from '~/serverless/store/state';
import { mockServerlessFunctions } from '../mock_data';
describe('Serverless Store Getters', () => {
diff --git a/spec/frontend/serverless/store/mutations_spec.js b/spec/frontend/serverless/store/mutations_spec.js
index e2771c7e5fd..a1a8f9a2ca7 100644
--- a/spec/frontend/serverless/store/mutations_spec.js
+++ b/spec/frontend/serverless/store/mutations_spec.js
@@ -1,5 +1,5 @@
-import mutations from '~/serverless/store/mutations';
import * as types from '~/serverless/store/mutation_types';
+import mutations from '~/serverless/store/mutations';
import { mockServerlessFunctions, mockMetrics } from '../mock_data';
describe('ServerlessMutations', () => {
diff --git a/spec/frontend/serverless/survey_banner_spec.js b/spec/frontend/serverless/survey_banner_spec.js
index 29b36fb9b5f..4682c2328c3 100644
--- a/spec/frontend/serverless/survey_banner_spec.js
+++ b/spec/frontend/serverless/survey_banner_spec.js
@@ -1,6 +1,6 @@
+import { GlBanner } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Cookies from 'js-cookie';
-import { GlBanner } from '@gitlab/ui';
import SurveyBanner from '~/serverless/survey_banner.vue';
describe('Knative survey banner', () => {
diff --git a/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js b/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js
index f3085fb7ffb..21b9721438d 100644
--- a/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js
+++ b/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlModal, GlFormCheckbox } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { initEmojiMock } from 'helpers/emoji';
import * as UserApi from '~/api/user_api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
diff --git a/spec/frontend/set_status_modal/user_availability_status_spec.js b/spec/frontend/set_status_modal/user_availability_status_spec.js
deleted file mode 100644
index 95ca0251ce0..00000000000
--- a/spec/frontend/set_status_modal/user_availability_status_spec.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import UserAvailabilityStatus from '~/set_status_modal/components/user_availability_status.vue';
-import { AVAILABILITY_STATUS } from '~/set_status_modal/utils';
-
-describe('UserAvailabilityStatus', () => {
- let wrapper;
-
- const createComponent = (props = {}) => {
- return shallowMount(UserAvailabilityStatus, {
- propsData: {
- ...props,
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('with availability status', () => {
- it(`set to ${AVAILABILITY_STATUS.BUSY}`, () => {
- wrapper = createComponent({ availability: AVAILABILITY_STATUS.BUSY });
- expect(wrapper.text()).toContain('(Busy)');
- });
-
- it(`set to ${AVAILABILITY_STATUS.NOT_SET}`, () => {
- wrapper = createComponent({ availability: AVAILABILITY_STATUS.NOT_SET });
- expect(wrapper.html()).toBe('');
- });
- });
-});
diff --git a/spec/frontend/set_status_modal/utils_spec.js b/spec/frontend/set_status_modal/utils_spec.js
new file mode 100644
index 00000000000..273f30f8311
--- /dev/null
+++ b/spec/frontend/set_status_modal/utils_spec.js
@@ -0,0 +1,15 @@
+import { AVAILABILITY_STATUS, isUserBusy } from '~/set_status_modal/utils';
+
+describe('Set status modal utils', () => {
+ describe('isUserBusy', () => {
+ it.each`
+ value | result
+ ${''} | ${false}
+ ${'fake status'} | ${false}
+ ${AVAILABILITY_STATUS.NOT_SET} | ${false}
+ ${AVAILABILITY_STATUS.BUSY} | ${true}
+ `('with $value returns $result', ({ value, result }) => {
+ expect(isUserBusy(value)).toBe(result);
+ });
+ });
+});
diff --git a/spec/frontend/settings_panels_spec.js b/spec/frontend/settings_panels_spec.js
index 2c5d91a45bc..8666106d3c6 100644
--- a/spec/frontend/settings_panels_spec.js
+++ b/spec/frontend/settings_panels_spec.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-import initSettingsPanels from '~/settings_panels';
+import initSettingsPanels, { isExpanded } from '~/settings_panels';
describe('Settings Panels', () => {
preloadFixtures('groups/edit.html');
@@ -20,11 +20,11 @@ describe('Settings Panels', () => {
// Our test environment automatically expands everything so we need to clear that out first
panel.classList.remove('expanded');
- expect(panel.classList.contains('expanded')).toBe(false);
+ expect(isExpanded(panel)).toBe(false);
initSettingsPanels();
- expect(panel.classList.contains('expanded')).toBe(true);
+ expect(isExpanded(panel)).toBe(true);
});
});
@@ -35,11 +35,11 @@ describe('Settings Panels', () => {
initSettingsPanels();
- expect(panel.classList.contains('expanded')).toBe(true);
+ expect(isExpanded(panel)).toBe(true);
$(trigger).click();
- expect(panel.classList.contains('expanded')).toBe(false);
+ expect(isExpanded(panel)).toBe(false);
expect(trigger.textContent).toEqual(originalText);
});
});
diff --git a/spec/frontend/sidebar/__snapshots__/todo_spec.js.snap b/spec/frontend/sidebar/__snapshots__/todo_spec.js.snap
index e295c587d70..846f45345e7 100644
--- a/spec/frontend/sidebar/__snapshots__/todo_spec.js.snap
+++ b/spec/frontend/sidebar/__snapshots__/todo_spec.js.snap
@@ -3,7 +3,7 @@
exports[`SidebarTodo template renders component container element with proper data attributes 1`] = `
<button
aria-label="Mark as done"
- class="btn btn-default btn-todo issuable-header-btn float-right"
+ class="gl-button btn btn-default btn-todo issuable-header-btn float-right"
data-issuable-id="1"
data-issuable-type="epic"
type="button"
diff --git a/spec/frontend/sidebar/assignee_title_spec.js b/spec/frontend/sidebar/assignee_title_spec.js
index 9f5d51c7795..3079cb28406 100644
--- a/spec/frontend/sidebar/assignee_title_spec.js
+++ b/spec/frontend/sidebar/assignee_title_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
import Component from '~/sidebar/components/assignees/assignee_title.vue';
diff --git a/spec/frontend/sidebar/assignees_realtime_spec.js b/spec/frontend/sidebar/assignees_realtime_spec.js
index 1c62c52dc67..0fab6a29f71 100644
--- a/spec/frontend/sidebar/assignees_realtime_spec.js
+++ b/spec/frontend/sidebar/assignees_realtime_spec.js
@@ -1,9 +1,9 @@
-import { shallowMount } from '@vue/test-utils';
import ActionCable from '@rails/actioncable';
+import { shallowMount } from '@vue/test-utils';
+import query from '~/issuable_sidebar/queries/issue_sidebar.query.graphql';
import AssigneesRealtime from '~/sidebar/components/assignees/assignees_realtime.vue';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import Mock from './mock_data';
-import query from '~/issuable_sidebar/queries/issue_sidebar.query.graphql';
jest.mock('@rails/actioncable', () => {
const mockConsumer = {
diff --git a/spec/frontend/sidebar/assignees_spec.js b/spec/frontend/sidebar/assignees_spec.js
index 23e82171fe9..74dce499999 100644
--- a/spec/frontend/sidebar/assignees_spec.js
+++ b/spec/frontend/sidebar/assignees_spec.js
@@ -1,6 +1,6 @@
+import { GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { trimText } from 'helpers/text_helper';
-import { GlIcon } from '@gitlab/ui';
import UsersMockHelper from 'helpers/user_mock_data_helper';
import Assignee from '~/sidebar/components/assignees/assignees.vue';
import UsersMock from './mock_data';
diff --git a/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
index 03d1ac3ab8d..5a3a152d201 100644
--- a/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
+++ b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
-import AssigneeAvatarLink from '~/sidebar/components/assignees/assignee_avatar_link.vue';
import AssigneeAvatar from '~/sidebar/components/assignees/assignee_avatar.vue';
+import AssigneeAvatarLink from '~/sidebar/components/assignees/assignee_avatar_link.vue';
import userDataMock from '../../user_data_mock';
const TOOLTIP_PLACEMENT = 'bottom';
@@ -79,4 +79,34 @@ describe('AssigneeAvatarLink component', () => {
});
},
);
+
+ describe.each`
+ tooltipHasName | availability | canMerge | expected
+ ${true} | ${'Busy'} | ${false} | ${'Root (Busy) (cannot merge)'}
+ ${true} | ${'Busy'} | ${true} | ${'Root (Busy)'}
+ ${true} | ${''} | ${false} | ${'Root (cannot merge)'}
+ ${true} | ${''} | ${true} | ${'Root'}
+ ${false} | ${'Busy'} | ${false} | ${'Cannot merge'}
+ ${false} | ${'Busy'} | ${true} | ${''}
+ ${false} | ${''} | ${false} | ${'Cannot merge'}
+ ${false} | ${''} | ${true} | ${''}
+ `(
+ "with tooltipHasName=$tooltipHasName and availability='$availability' and canMerge=$canMerge",
+ ({ tooltipHasName, availability, canMerge, expected }) => {
+ beforeEach(() => {
+ createComponent({
+ tooltipHasName,
+ user: {
+ ...userDataMock(),
+ can_merge: canMerge,
+ availability,
+ },
+ });
+ });
+
+ it('sets tooltip to $expected', () => {
+ expect(findTooltipText()).toBe(expected);
+ });
+ },
+ );
});
diff --git a/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js b/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js
index 8e158c99971..5aa8264b98c 100644
--- a/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js
+++ b/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js
@@ -1,8 +1,8 @@
-import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import UsersMockHelper from 'helpers/user_mock_data_helper';
-import CollapsedAssigneeList from '~/sidebar/components/assignees/collapsed_assignee_list.vue';
import CollapsedAssignee from '~/sidebar/components/assignees/collapsed_assignee.vue';
+import CollapsedAssigneeList from '~/sidebar/components/assignees/collapsed_assignee_list.vue';
const DEFAULT_MAX_COUNTER = 99;
@@ -187,4 +187,26 @@ describe('CollapsedAssigneeList component', () => {
expect(findAvatarCounter().text()).toEqual(`${DEFAULT_MAX_COUNTER}+`);
});
});
+
+ const [busyUser] = UsersMockHelper.createNumberRandomUsers(1);
+ const [canMergeUser] = UsersMockHelper.createNumberRandomUsers(1);
+ busyUser.availability = 'busy';
+ canMergeUser.can_merge = true;
+
+ describe.each`
+ users | busy | canMerge | expected
+ ${[busyUser, canMergeUser]} | ${1} | ${1} | ${`${busyUser.name} (Busy), ${canMergeUser.name} (1/2 can merge)`}
+ ${[busyUser]} | ${1} | ${0} | ${`${busyUser.name} (Busy) (cannot merge)`}
+ ${[canMergeUser]} | ${0} | ${1} | ${`${canMergeUser.name}`}
+ ${[]} | ${0} | ${0} | ${'Assignee(s)'}
+ `(
+ 'with $users.length users, $busy is busy and $canMerge that can merge',
+ ({ users, expected }) => {
+ it('generates the tooltip text', () => {
+ createComponent({ users });
+
+ expect(getTooltipTitle()).toEqual(expected);
+ });
+ },
+ );
});
diff --git a/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js b/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js
index ee1f8ed8d2b..b49e6255923 100644
--- a/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js
+++ b/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js
@@ -1,6 +1,7 @@
import { shallowMount } from '@vue/test-utils';
-import CollapsedAssignee from '~/sidebar/components/assignees/collapsed_assignee.vue';
import AssigneeAvatar from '~/sidebar/components/assignees/assignee_avatar.vue';
+import CollapsedAssignee from '~/sidebar/components/assignees/collapsed_assignee.vue';
+import UserNameWithStatus from '~/sidebar/components/assignees/user_name_with_status.vue';
import userDataMock from '../../user_data_mock';
const TEST_USER = userDataMock();
@@ -18,6 +19,9 @@ describe('CollapsedAssignee assignee component', () => {
wrapper = shallowMount(CollapsedAssignee, {
propsData,
+ stubs: {
+ UserNameWithStatus,
+ },
});
}
diff --git a/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js
new file mode 100644
index 00000000000..4ee12838491
--- /dev/null
+++ b/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js
@@ -0,0 +1,120 @@
+import { GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
+
+describe('boards sidebar remove issue', () => {
+ let wrapper;
+
+ const findLoader = () => wrapper.find(GlLoadingIcon);
+ const findEditButton = () => wrapper.find('[data-testid="edit-button"]');
+ const findTitle = () => wrapper.find('[data-testid="title"]');
+ const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]');
+ const findExpanded = () => wrapper.find('[data-testid="expanded-content"]');
+
+ const createComponent = ({ props = {}, slots = {}, canUpdate = false } = {}) => {
+ wrapper = shallowMount(SidebarEditableItem, {
+ attachTo: document.body,
+ provide: { canUpdate },
+ propsData: props,
+ slots,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('template', () => {
+ it('renders title', () => {
+ const title = 'Sidebar item title';
+ createComponent({ props: { title } });
+
+ expect(findTitle().text()).toBe(title);
+ });
+
+ it('hides edit button, loader and expanded content by default', () => {
+ createComponent();
+
+ expect(findEditButton().exists()).toBe(false);
+ expect(findLoader().exists()).toBe(false);
+ expect(findExpanded().isVisible()).toBe(false);
+ });
+
+ it('shows "None" if empty collapsed slot', () => {
+ createComponent();
+
+ expect(findCollapsed().text()).toBe('None');
+ });
+
+ it('renders collapsed content by default', () => {
+ const slots = { collapsed: '<div>Collapsed content</div>' };
+ createComponent({ slots });
+
+ expect(findCollapsed().text()).toBe('Collapsed content');
+ });
+
+ it('shows edit button if can update', () => {
+ createComponent({ canUpdate: true });
+
+ expect(findEditButton().exists()).toBe(true);
+ });
+
+ it('shows loading icon if loading', () => {
+ createComponent({ props: { loading: true } });
+
+ expect(findLoader().exists()).toBe(true);
+ });
+
+ it('shows expanded content and hides collapsed content when clicking edit button', async () => {
+ const slots = { default: '<div>Select item</div>' };
+ createComponent({ canUpdate: true, slots });
+ findEditButton().vm.$emit('click');
+
+ await wrapper.vm.$nextTick;
+
+ expect(findCollapsed().isVisible()).toBe(false);
+ expect(findExpanded().isVisible()).toBe(true);
+ });
+ });
+
+ describe('collapsing an item by offclicking', () => {
+ beforeEach(async () => {
+ createComponent({ canUpdate: true });
+ findEditButton().vm.$emit('click');
+ await wrapper.vm.$nextTick();
+ });
+
+ it('hides expanded section and displays collapsed section', async () => {
+ expect(findExpanded().isVisible()).toBe(true);
+ document.body.click();
+
+ await wrapper.vm.$nextTick();
+
+ expect(findCollapsed().isVisible()).toBe(true);
+ expect(findExpanded().isVisible()).toBe(false);
+ });
+ });
+
+ it('emits open when edit button is clicked and edit is initailized to false', async () => {
+ createComponent({ canUpdate: true });
+
+ findEditButton().vm.$emit('click');
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.emitted().open.length).toBe(1);
+ });
+
+ it('does not emits events when collapsing with false `emitEvent`', async () => {
+ createComponent({ canUpdate: true });
+
+ findEditButton().vm.$emit('click');
+
+ await wrapper.vm.$nextTick();
+
+ wrapper.vm.collapse({ emitEvent: false });
+
+ expect(wrapper.emitted().close).toBeUndefined();
+ });
+});
diff --git a/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js b/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js
index da69f56d442..7e81df1d7d2 100644
--- a/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js
+++ b/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js
@@ -1,8 +1,8 @@
import { mount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
import UsersMockHelper from 'helpers/user_mock_data_helper';
-import UncollapsedAssigneeList from '~/sidebar/components/assignees/uncollapsed_assignee_list.vue';
import AssigneeAvatarLink from '~/sidebar/components/assignees/assignee_avatar_link.vue';
+import UncollapsedAssigneeList from '~/sidebar/components/assignees/uncollapsed_assignee_list.vue';
import userDataMock from '../../user_data_mock';
const DEFAULT_RENDER_COUNT = 5;
diff --git a/spec/frontend/sidebar/components/assignees/user_name_with_status_spec.js b/spec/frontend/sidebar/components/assignees/user_name_with_status_spec.js
new file mode 100644
index 00000000000..9483c6624c5
--- /dev/null
+++ b/spec/frontend/sidebar/components/assignees/user_name_with_status_spec.js
@@ -0,0 +1,51 @@
+import { GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { AVAILABILITY_STATUS } from '~/set_status_modal/utils';
+import UserNameWithStatus from '~/sidebar/components/assignees/user_name_with_status.vue';
+
+const name = 'Goku';
+const containerClasses = 'gl-cool-class gl-over-9000';
+
+describe('UserNameWithStatus', () => {
+ let wrapper;
+
+ function createComponent(props = {}) {
+ return shallowMount(UserNameWithStatus, {
+ propsData: { name, containerClasses, ...props },
+ stubs: {
+ GlSprintf,
+ },
+ });
+ }
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('will render the users name', () => {
+ expect(wrapper.html()).toContain(name);
+ });
+
+ it('will not render "Busy"', () => {
+ expect(wrapper.html()).not.toContain('Busy');
+ });
+
+ it('will render all relevant containerClasses', () => {
+ const classes = wrapper.find('span').classes().join(' ');
+ expect(classes).toBe(containerClasses);
+ });
+
+ describe(`with availability="${AVAILABILITY_STATUS.BUSY}"`, () => {
+ beforeEach(() => {
+ wrapper = createComponent({ availability: AVAILABILITY_STATUS.BUSY });
+ });
+
+ it('will render "Busy"', () => {
+ expect(wrapper.html()).toContain('Goku (Busy)');
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/components/copy_email_to_clipboard_spec.js b/spec/frontend/sidebar/components/copy_email_to_clipboard_spec.js
index b9132fa4450..704847f65bf 100644
--- a/spec/frontend/sidebar/components/copy_email_to_clipboard_spec.js
+++ b/spec/frontend/sidebar/components/copy_email_to_clipboard_spec.js
@@ -1,7 +1,7 @@
-import { mount } from '@vue/test-utils';
import { getByText } from '@testing-library/dom';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import { mount } from '@vue/test-utils';
import CopyEmailToClipboard from '~/sidebar/components/copy_email_to_clipboard.vue';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
describe('CopyEmailToClipboard component', () => {
const sampleEmail = 'sample+email@test.com';
diff --git a/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js b/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js
new file mode 100644
index 00000000000..7c67149b517
--- /dev/null
+++ b/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js
@@ -0,0 +1,91 @@
+import { shallowMount } from '@vue/test-utils';
+import { TEST_HOST } from 'helpers/test_constants';
+import ReviewerAvatarLink from '~/sidebar/components/reviewers/reviewer_avatar_link.vue';
+import UncollapsedReviewerList from '~/sidebar/components/reviewers/uncollapsed_reviewer_list.vue';
+import userDataMock from '../../user_data_mock';
+
+describe('UncollapsedReviewerList component', () => {
+ let wrapper;
+
+ function createComponent(props = {}) {
+ const propsData = {
+ users: [],
+ rootPath: TEST_HOST,
+ ...props,
+ };
+
+ wrapper = shallowMount(UncollapsedReviewerList, {
+ propsData,
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('single reviewer', () => {
+ beforeEach(() => {
+ const user = userDataMock();
+
+ createComponent({
+ users: [user],
+ });
+ });
+
+ it('only has one user', () => {
+ expect(wrapper.findAll(ReviewerAvatarLink).length).toBe(1);
+ });
+
+ it('shows one user with avatar, username and author name', () => {
+ expect(wrapper.text()).toContain(`@root`);
+ });
+
+ it('renders re-request loading icon', async () => {
+ await wrapper.setData({ loadingStates: { 1: 'loading' } });
+
+ expect(wrapper.find('[data-testid="re-request-button"]').props('loading')).toBe(true);
+ });
+
+ it('renders re-request success icon', async () => {
+ await wrapper.setData({ loadingStates: { 1: 'success' } });
+
+ expect(wrapper.find('[data-testid="re-request-success"]').exists()).toBe(true);
+ });
+ });
+
+ describe('multiple reviewers', () => {
+ beforeEach(() => {
+ const user = userDataMock();
+
+ createComponent({
+ users: [user, { ...user, id: 2, username: 'hello-world' }],
+ });
+ });
+
+ it('only has one user', () => {
+ expect(wrapper.findAll(ReviewerAvatarLink).length).toBe(2);
+ });
+
+ it('shows one user with avatar, username and author name', () => {
+ expect(wrapper.text()).toContain(`@root`);
+ expect(wrapper.text()).toContain(`@hello-world`);
+ });
+
+ it('renders re-request loading icon', async () => {
+ await wrapper.setData({ loadingStates: { 2: 'loading' } });
+
+ expect(wrapper.findAll('[data-testid="re-request-button"]').length).toBe(2);
+ expect(wrapper.findAll('[data-testid="re-request-button"]').at(1).props('loading')).toBe(
+ true,
+ );
+ });
+
+ it('renders re-request success icon', async () => {
+ await wrapper.setData({ loadingStates: { 2: 'success' } });
+
+ expect(wrapper.findAll('[data-testid="re-request-button"]').length).toBe(1);
+ expect(wrapper.findAll('[data-testid="re-request-success"]').length).toBe(1);
+ expect(wrapper.find('[data-testid="re-request-success"]').exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/components/severity/severity_spec.js b/spec/frontend/sidebar/components/severity/severity_spec.js
index ba7cea0919c..1e4624e4dcd 100644
--- a/spec/frontend/sidebar/components/severity/severity_spec.js
+++ b/spec/frontend/sidebar/components/severity/severity_spec.js
@@ -1,7 +1,7 @@
-import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
-import SeverityToken from '~/sidebar/components/severity/severity.vue';
+import { shallowMount } from '@vue/test-utils';
import { INCIDENT_SEVERITY } from '~/sidebar/components/severity/constants';
+import SeverityToken from '~/sidebar/components/severity/severity.vue';
describe('SeverityToken', () => {
let wrapper;
diff --git a/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js b/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js
index fa40d75d4e9..747d370e1cf 100644
--- a/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js
+++ b/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js
@@ -1,11 +1,11 @@
-import { shallowMount } from '@vue/test-utils';
import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlTooltip, GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
-import SidebarSeverity from '~/sidebar/components/severity/sidebar_severity.vue';
-import SeverityToken from '~/sidebar/components/severity/severity.vue';
-import updateIssuableSeverity from '~/sidebar/components/severity/graphql/mutations/update_issuable_severity.mutation.graphql';
import { INCIDENT_SEVERITY, ISSUABLE_TYPES } from '~/sidebar/components/severity/constants';
+import updateIssuableSeverity from '~/sidebar/components/severity/graphql/mutations/update_issuable_severity.mutation.graphql';
+import SeverityToken from '~/sidebar/components/severity/severity.vue';
+import SidebarSeverity from '~/sidebar/components/severity/sidebar_severity.vue';
jest.mock('~/flash');
diff --git a/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js b/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
index 0b6a2e6ceb9..4d03aedf1be 100644
--- a/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
+++ b/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
@@ -1,6 +1,6 @@
-import { createMockDirective } from 'helpers/vue_mock_directive';
import { mount } from '@vue/test-utils';
import { stubTransition } from 'helpers/stub_transition';
+import { createMockDirective } from 'helpers/vue_mock_directive';
import TimeTracker from '~/sidebar/components/time_tracking/time_tracker.vue';
describe('Issuable Time Tracker', () => {
diff --git a/spec/frontend/sidebar/confidential/edit_form_buttons_spec.js b/spec/frontend/sidebar/confidential/edit_form_buttons_spec.js
index 8c868205295..427e3a89c29 100644
--- a/spec/frontend/sidebar/confidential/edit_form_buttons_spec.js
+++ b/spec/frontend/sidebar/confidential/edit_form_buttons_spec.js
@@ -1,9 +1,9 @@
import { shallowMount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
+import { deprecatedCreateFlash as flash } from '~/flash';
+import createStore from '~/notes/stores';
import EditFormButtons from '~/sidebar/components/confidential/edit_form_buttons.vue';
import eventHub from '~/sidebar/event_hub';
-import createStore from '~/notes/stores';
-import { deprecatedCreateFlash as flash } from '~/flash';
jest.mock('~/sidebar/event_hub', () => ({ $emit: jest.fn() }));
jest.mock('~/flash');
diff --git a/spec/frontend/sidebar/confidential_issue_sidebar_spec.js b/spec/frontend/sidebar/confidential_issue_sidebar_spec.js
index bc2df9305d0..93a6401b1fc 100644
--- a/spec/frontend/sidebar/confidential_issue_sidebar_spec.js
+++ b/spec/frontend/sidebar/confidential_issue_sidebar_spec.js
@@ -1,10 +1,10 @@
import { shallowMount } from '@vue/test-utils';
-import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
-import ConfidentialIssueSidebar from '~/sidebar/components/confidential/confidential_issue_sidebar.vue';
-import EditForm from '~/sidebar/components/confidential/edit_form.vue';
+import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
import createStore from '~/notes/stores';
import * as types from '~/notes/stores/mutation_types';
+import ConfidentialIssueSidebar from '~/sidebar/components/confidential/confidential_issue_sidebar.vue';
+import EditForm from '~/sidebar/components/confidential/edit_form.vue';
jest.mock('~/flash');
jest.mock('~/sidebar/services/sidebar_service');
diff --git a/spec/frontend/sidebar/lock/edit_form_buttons_spec.js b/spec/frontend/sidebar/lock/edit_form_buttons_spec.js
index a14ca711204..49283ea99cf 100644
--- a/spec/frontend/sidebar/lock/edit_form_buttons_spec.js
+++ b/spec/frontend/sidebar/lock/edit_form_buttons_spec.js
@@ -1,9 +1,9 @@
import { mount } from '@vue/test-utils';
-import EditFormButtons from '~/sidebar/components/lock/edit_form_buttons.vue';
-import eventHub from '~/sidebar/event_hub';
import { deprecatedCreateFlash as flash } from '~/flash';
-import createStore from '~/notes/stores';
import { createStore as createMrStore } from '~/mr_notes/stores';
+import createStore from '~/notes/stores';
+import EditFormButtons from '~/sidebar/components/lock/edit_form_buttons.vue';
+import eventHub from '~/sidebar/event_hub';
import { ISSUABLE_TYPE_ISSUE, ISSUABLE_TYPE_MR } from './constants';
jest.mock('~/sidebar/event_hub', () => ({ $emit: jest.fn() }));
diff --git a/spec/frontend/sidebar/lock/issuable_lock_form_spec.js b/spec/frontend/sidebar/lock/issuable_lock_form_spec.js
index 92cdba4f1f2..1743e114bb0 100644
--- a/spec/frontend/sidebar/lock/issuable_lock_form_spec.js
+++ b/spec/frontend/sidebar/lock/issuable_lock_form_spec.js
@@ -1,10 +1,10 @@
import { shallowMount } from '@vue/test-utils';
import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import IssuableLockForm from '~/sidebar/components/lock/issuable_lock_form.vue';
-import EditForm from '~/sidebar/components/lock/edit_form.vue';
-import createStore from '~/notes/stores';
import { createStore as createMrStore } from '~/mr_notes/stores';
+import createStore from '~/notes/stores';
+import EditForm from '~/sidebar/components/lock/edit_form.vue';
+import IssuableLockForm from '~/sidebar/components/lock/issuable_lock_form.vue';
import { ISSUABLE_TYPE_ISSUE, ISSUABLE_TYPE_MR } from './constants';
describe('IssuableLockForm', () => {
diff --git a/spec/frontend/sidebar/reviewer_title_spec.js b/spec/frontend/sidebar/reviewer_title_spec.js
index cbd36040579..3c250be5d5e 100644
--- a/spec/frontend/sidebar/reviewer_title_spec.js
+++ b/spec/frontend/sidebar/reviewer_title_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
import Component from '~/sidebar/components/reviewers/reviewer_title.vue';
diff --git a/spec/frontend/sidebar/reviewers_spec.js b/spec/frontend/sidebar/reviewers_spec.js
index 91f28e85f3b..fc24b51287f 100644
--- a/spec/frontend/sidebar/reviewers_spec.js
+++ b/spec/frontend/sidebar/reviewers_spec.js
@@ -1,6 +1,6 @@
+import { GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { trimText } from 'helpers/text_helper';
-import { GlIcon } from '@gitlab/ui';
import UsersMockHelper from 'helpers/user_mock_data_helper';
import Reviewer from '~/sidebar/components/reviewers/reviewers.vue';
import UsersMock from './mock_data';
@@ -114,8 +114,7 @@ describe('Reviewer component', () => {
editable: true,
});
- expect(wrapper.findAll('.user-item').length).toBe(users.length);
- expect(wrapper.find('.user-list-more').exists()).toBe(false);
+ expect(wrapper.findAll('[data-testid="reviewer"]').length).toBe(users.length);
});
it('shows sorted reviewer where "can merge" users are sorted first', () => {
@@ -144,10 +143,10 @@ describe('Reviewer component', () => {
users,
});
- const userItems = wrapper.findAll('.user-list .user-item a');
+ const userItems = wrapper.findAll('[data-testid="reviewer"]');
expect(userItems.length).toBe(3);
- expect(userItems.at(0).attributes('title')).toBe(users[2].name);
+ expect(userItems.at(0).find('a').attributes('title')).toBe(users[2].name);
});
it('passes the sorted reviewers to the collapsed-reviewer-list', () => {
diff --git a/spec/frontend/sidebar/sidebar_assignees_spec.js b/spec/frontend/sidebar/sidebar_assignees_spec.js
index f1c13a5f818..e737b57e33d 100644
--- a/spec/frontend/sidebar/sidebar_assignees_spec.js
+++ b/spec/frontend/sidebar/sidebar_assignees_spec.js
@@ -1,11 +1,11 @@
import { shallowMount } from '@vue/test-utils';
-import AxiosMockAdapter from 'axios-mock-adapter';
import axios from 'axios';
-import SidebarAssignees from '~/sidebar/components/assignees/sidebar_assignees.vue';
+import AxiosMockAdapter from 'axios-mock-adapter';
import Assigness from '~/sidebar/components/assignees/assignees.vue';
import AssigneesRealtime from '~/sidebar/components/assignees/assignees_realtime.vue';
-import SidebarMediator from '~/sidebar/sidebar_mediator';
+import SidebarAssignees from '~/sidebar/components/assignees/sidebar_assignees.vue';
import SidebarService from '~/sidebar/services/sidebar_service';
+import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarStore from '~/sidebar/stores/sidebar_store';
import Mock from './mock_data';
diff --git a/spec/frontend/sidebar/sidebar_move_issue_spec.js b/spec/frontend/sidebar/sidebar_move_issue_spec.js
index 24bb5a8e916..6a7758ace40 100644
--- a/spec/frontend/sidebar/sidebar_move_issue_spec.js
+++ b/spec/frontend/sidebar/sidebar_move_issue_spec.js
@@ -1,10 +1,10 @@
-import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter';
+import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
+import SidebarMoveIssue from '~/sidebar/lib/sidebar_move_issue';
+import SidebarService from '~/sidebar/services/sidebar_service';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarStore from '~/sidebar/stores/sidebar_store';
-import SidebarService from '~/sidebar/services/sidebar_service';
-import SidebarMoveIssue from '~/sidebar/lib/sidebar_move_issue';
import Mock from './mock_data';
describe('SidebarMoveIssue', () => {
diff --git a/spec/frontend/sidebar/sidebar_subscriptions_spec.js b/spec/frontend/sidebar/sidebar_subscriptions_spec.js
index 18aaeabe3dd..d900fde7e70 100644
--- a/spec/frontend/sidebar/sidebar_subscriptions_spec.js
+++ b/spec/frontend/sidebar/sidebar_subscriptions_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import SidebarSubscriptions from '~/sidebar/components/subscriptions/sidebar_subscriptions.vue';
-import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarService from '~/sidebar/services/sidebar_service';
+import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarStore from '~/sidebar/stores/sidebar_store';
import Mock from './mock_data';
diff --git a/spec/frontend/sidebar/subscriptions_spec.js b/spec/frontend/sidebar/subscriptions_spec.js
index 043ffd972da..e7ae59e26cf 100644
--- a/spec/frontend/sidebar/subscriptions_spec.js
+++ b/spec/frontend/sidebar/subscriptions_spec.js
@@ -1,17 +1,20 @@
+import { GlToggle } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import Subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue';
import eventHub from '~/sidebar/event_hub';
-import ToggleButton from '~/vue_shared/components/toggle_button.vue';
describe('Subscriptions', () => {
let wrapper;
- const findToggleButton = () => wrapper.find(ToggleButton);
+ const findToggleButton = () => wrapper.findComponent(GlToggle);
const mountComponent = (propsData) =>
- shallowMount(Subscriptions, {
- propsData,
- });
+ extendedWrapper(
+ shallowMount(Subscriptions, {
+ propsData,
+ }),
+ );
afterEach(() => {
wrapper.destroy();
@@ -24,7 +27,7 @@ describe('Subscriptions', () => {
subscribed: undefined,
});
- expect(findToggleButton().attributes('isloading')).toBe('true');
+ expect(findToggleButton().props('isLoading')).toBe(true);
});
it('is toggled "off" when currently not subscribed', () => {
@@ -32,7 +35,7 @@ describe('Subscriptions', () => {
subscribed: false,
});
- expect(findToggleButton().attributes('value')).toBeFalsy();
+ expect(findToggleButton().props('value')).toBe(false);
});
it('is toggled "on" when currently subscribed', () => {
@@ -40,7 +43,7 @@ describe('Subscriptions', () => {
subscribed: true,
});
- expect(findToggleButton().attributes('value')).toBe('true');
+ expect(findToggleButton().props('value')).toBe(true);
});
it('toggleSubscription method emits `toggleSubscription` event on eventHub and Component', () => {
@@ -93,14 +96,16 @@ describe('Subscriptions', () => {
});
it('sets the correct display text', () => {
- expect(wrapper.find('.issuable-header-text').text()).toContain(subscribeDisabledDescription);
+ expect(wrapper.findByTestId('subscription-title').text()).toContain(
+ subscribeDisabledDescription,
+ );
expect(wrapper.find({ ref: 'tooltip' }).attributes('title')).toBe(
subscribeDisabledDescription,
);
});
it('does not render the toggle button', () => {
- expect(wrapper.find('.js-issuable-subscribe-button').exists()).toBe(false);
+ expect(findToggleButton().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/sidebar/todo_spec.js b/spec/frontend/sidebar/todo_spec.js
index 4adfaf7ad7b..ff6da3abad0 100644
--- a/spec/frontend/sidebar/todo_spec.js
+++ b/spec/frontend/sidebar/todo_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import SidebarTodos from '~/sidebar/components/todo_toggle/todo.vue';
@@ -26,7 +26,7 @@ describe('SidebarTodo', () => {
it.each`
state | classes
- ${false} | ${['btn', 'btn-default', 'btn-todo', 'issuable-header-btn', 'float-right']}
+ ${false} | ${['gl-button', 'btn', 'btn-default', 'btn-todo', 'issuable-header-btn', 'float-right']}
${true} | ${['btn-blank', 'btn-todo', 'sidebar-collapsed-icon', 'dont-change-state']}
`('returns todo button classes for when `collapsed` prop is `$state`', ({ state, classes }) => {
createComponent({ collapsed: state });
@@ -35,7 +35,7 @@ describe('SidebarTodo', () => {
it.each`
isTodo | iconClass | label | icon
- ${false} | ${''} | ${'Add a To-Do'} | ${'todo-add'}
+ ${false} | ${''} | ${'Add a to do'} | ${'todo-add'}
${true} | ${'todo-undone'} | ${'Mark as done'} | ${'todo-done'}
`(
'renders proper button when `isTodo` prop is `$isTodo`',
diff --git a/spec/frontend/sidebar/user_data_mock.js b/spec/frontend/sidebar/user_data_mock.js
index df90a65f6f9..41d0331f34a 100644
--- a/spec/frontend/sidebar/user_data_mock.js
+++ b/spec/frontend/sidebar/user_data_mock.js
@@ -8,4 +8,6 @@ export default () => ({
username: 'root',
web_url: `${TEST_HOST}/root`,
can_merge: true,
+ can_update_merge_request: true,
+ reviewed: true,
});
diff --git a/spec/frontend/snippets/components/edit_spec.js b/spec/frontend/snippets/components/edit_spec.js
index b818f98efb1..2b6d3ca8c2a 100644
--- a/spec/frontend/snippets/components/edit_spec.js
+++ b/spec/frontend/snippets/components/edit_spec.js
@@ -1,138 +1,120 @@
-import VueApollo, { ApolloMutation } from 'vue-apollo';
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
-import waitForPromises from 'helpers/wait_for_promises';
+import { merge } from 'lodash';
+import { nextTick } from 'vue';
+import VueApollo, { ApolloMutation } from 'vue-apollo';
+import { useFakeDate } from 'helpers/fake_date';
import createMockApollo from 'helpers/mock_apollo_helper';
+import { stubComponent } from 'helpers/stub_component';
+import waitForPromises from 'helpers/wait_for_promises';
import GetSnippetQuery from 'shared_queries/snippet/snippet.query.graphql';
+import CaptchaModal from '~/captcha/captcha_modal.vue';
import { deprecatedCreateFlash as Flash } from '~/flash';
import * as urlUtils from '~/lib/utils/url_utility';
import SnippetEditApp from '~/snippets/components/edit.vue';
+import SnippetBlobActionsEdit from '~/snippets/components/snippet_blob_actions_edit.vue';
import SnippetDescriptionEdit from '~/snippets/components/snippet_description_edit.vue';
import SnippetVisibilityEdit from '~/snippets/components/snippet_visibility_edit.vue';
-import SnippetBlobActionsEdit from '~/snippets/components/snippet_blob_actions_edit.vue';
-import TitleField from '~/vue_shared/components/form/title.vue';
-import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue';
import {
SNIPPET_VISIBILITY_PRIVATE,
SNIPPET_VISIBILITY_INTERNAL,
SNIPPET_VISIBILITY_PUBLIC,
} from '~/snippets/constants';
-import UpdateSnippetMutation from '~/snippets/mutations/updateSnippet.mutation.graphql';
import CreateSnippetMutation from '~/snippets/mutations/createSnippet.mutation.graphql';
-import { testEntries } from '../test_utils';
+import UpdateSnippetMutation from '~/snippets/mutations/updateSnippet.mutation.graphql';
+import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue';
+import TitleField from '~/vue_shared/components/form/title.vue';
+import { testEntries, createGQLSnippetsQueryResponse, createGQLSnippet } from '../test_utils';
jest.mock('~/flash');
const TEST_UPLOADED_FILES = ['foo/bar.txt', 'alpha/beta.js'];
-const TEST_API_ERROR = 'Ufff';
-const TEST_MUTATION_ERROR = 'Bummer';
-
+const TEST_API_ERROR = new Error('TEST_API_ERROR');
+const TEST_MUTATION_ERROR = 'Test mutation error';
+const TEST_CAPTCHA_RESPONSE = 'i-got-a-captcha';
+const TEST_CAPTCHA_SITE_KEY = 'abc123';
const TEST_ACTIONS = {
- NO_CONTENT: {
- ...testEntries.created.diff,
- content: '',
- },
- NO_PATH: {
- ...testEntries.created.diff,
- filePath: '',
- },
- VALID: {
- ...testEntries.created.diff,
- },
+ NO_CONTENT: merge({}, testEntries.created.diff, { content: '' }),
+ NO_PATH: merge({}, testEntries.created.diff, { filePath: '' }),
+ VALID: merge({}, testEntries.created.diff),
};
-
const TEST_WEB_URL = '/snippets/7';
+const TEST_SNIPPET_GID = 'gid://gitlab/PersonalSnippet/42';
+
+const createSnippet = () =>
+ merge(createGQLSnippet(), {
+ webUrl: TEST_WEB_URL,
+ visibilityLevel: SNIPPET_VISIBILITY_PRIVATE,
+ });
+
+const createQueryResponse = (obj = {}) =>
+ createGQLSnippetsQueryResponse([merge(createSnippet(), obj)]);
+
+const createMutationResponse = (key, obj = {}) => ({
+ data: {
+ [key]: merge(
+ {
+ errors: [],
+ snippet: {
+ __typename: 'Snippet',
+ webUrl: TEST_WEB_URL,
+ },
+ spamLogId: null,
+ needsCaptchaResponse: false,
+ captchaSiteKey: null,
+ },
+ obj,
+ ),
+ },
+});
+
+const createMutationResponseWithErrors = (key) =>
+ createMutationResponse(key, { errors: [TEST_MUTATION_ERROR] });
+
+const createMutationResponseWithRecaptcha = (key) =>
+ createMutationResponse(key, {
+ errors: ['ignored captcha error message'],
+ needsCaptchaResponse: true,
+ captchaSiteKey: TEST_CAPTCHA_SITE_KEY,
+ });
-const createTestSnippet = () => ({
- webUrl: TEST_WEB_URL,
- id: 7,
- title: 'Snippet Title',
- description: 'Lorem ipsum snippet desc',
- visibilityLevel: SNIPPET_VISIBILITY_PRIVATE,
+const getApiData = ({
+ id,
+ title = '',
+ description = '',
+ visibilityLevel = SNIPPET_VISIBILITY_PRIVATE,
+} = {}) => ({
+ id,
+ title,
+ description,
+ visibilityLevel,
+ blobActions: [],
});
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
describe('Snippet Edit app', () => {
+ useFakeDate();
+
let wrapper;
- let fakeApollo;
+ let getSpy;
+
+ // Mutate spy receives a "key" so that we can:
+ // - Use the same spy whether we are creating or updating.
+ // - Build the correct response object
+ // - Assert which mutation was sent
+ let mutateSpy;
+
const relativeUrlRoot = '/foo/';
const originalRelativeUrlRoot = gon.relative_url_root;
- const GetSnippetQuerySpy = jest.fn().mockResolvedValue({
- data: { snippets: { nodes: [createTestSnippet()] } },
- });
- const mutationTypes = {
- RESOLVE: jest.fn().mockResolvedValue({
- data: {
- updateSnippet: {
- errors: [],
- snippet: createTestSnippet(),
- },
- },
- }),
- RESOLVE_WITH_ERRORS: jest.fn().mockResolvedValue({
- data: {
- updateSnippet: {
- errors: [TEST_MUTATION_ERROR],
- snippet: createTestSnippet(),
- },
- createSnippet: {
- errors: [TEST_MUTATION_ERROR],
- snippet: null,
- },
- },
- }),
- REJECT: jest.fn().mockRejectedValue(TEST_API_ERROR),
- };
-
- function createComponent({
- props = {},
- loading = false,
- mutationRes = mutationTypes.RESOLVE,
- selectedLevel = SNIPPET_VISIBILITY_PRIVATE,
- withApollo = false,
- } = {}) {
- let componentData = {
- mocks: {
- $apollo: {
- queries: {
- snippet: { loading },
- },
- mutate: mutationRes,
- },
- },
- };
-
- if (withApollo) {
- const localVue = createLocalVue();
- localVue.use(VueApollo);
-
- const requestHandlers = [[GetSnippetQuery, GetSnippetQuerySpy]];
- fakeApollo = createMockApollo(requestHandlers);
- componentData = {
- localVue,
- apolloProvider: fakeApollo,
- };
- }
+ beforeEach(() => {
+ getSpy = jest.fn().mockResolvedValue(createQueryResponse());
- wrapper = shallowMount(SnippetEditApp, {
- ...componentData,
- stubs: {
- ApolloMutation,
- FormFooterActions,
- },
- provide: {
- selectedLevel,
- },
- propsData: {
- snippetGid: 'gid://gitlab/PersonalSnippet/42',
- markdownPreviewPath: 'http://preview.foo.bar',
- markdownDocsPath: 'http://docs.foo.bar',
- ...props,
- },
- });
- }
+ // See `mutateSpy` declaration comment for why we send a key
+ mutateSpy = jest.fn().mockImplementation((key) => Promise.resolve(createMutationResponse(key)));
- beforeEach(() => {
gon.relative_url_root = relativeUrlRoot;
jest.spyOn(urlUtils, 'redirectTo').mockImplementation();
});
@@ -144,10 +126,10 @@ describe('Snippet Edit app', () => {
});
const findBlobActions = () => wrapper.find(SnippetBlobActionsEdit);
+ const findCaptchaModal = () => wrapper.find(CaptchaModal);
const findSubmitButton = () => wrapper.find('[data-testid="snippet-submit-btn"]');
const findCancelButton = () => wrapper.find('[data-testid="snippet-cancel-btn"]');
const hasDisabledSubmit = () => Boolean(findSubmitButton().attributes('disabled'));
-
const clickSubmitBtn = () => wrapper.find('[data-testid="snippet-edit-form"]').trigger('submit');
const triggerBlobActions = (actions) => findBlobActions().vm.$emit('actions', actions);
const setUploadFilesHtml = (paths) => {
@@ -155,53 +137,92 @@ describe('Snippet Edit app', () => {
.map((path) => `<input name="files[]" value="${path}">`)
.join('');
};
- const getApiData = ({
- id,
- title = '',
- description = '',
- visibilityLevel = SNIPPET_VISIBILITY_PRIVATE,
- } = {}) => ({
- id,
- title,
- description,
- visibilityLevel,
- blobActions: [],
- });
+ const setTitle = (val) => wrapper.find(TitleField).vm.$emit('input', val);
+ const setDescription = (val) => wrapper.find(SnippetDescriptionEdit).vm.$emit('input', val);
- // Ideally we wouldn't call this method directly, but we don't have a way to trigger
- // apollo responses yet.
- const loadSnippet = (...nodes) => {
- if (nodes.length) {
- wrapper.setData({
- snippet: nodes[0],
- newSnippet: false,
- });
- } else {
- wrapper.setData({
- newSnippet: true,
- });
+ const createComponent = ({ props = {}, selectedLevel = SNIPPET_VISIBILITY_PRIVATE } = {}) => {
+ if (wrapper) {
+ throw new Error('wrapper already created');
}
+
+ const requestHandlers = [
+ [GetSnippetQuery, getSpy],
+ // See `mutateSpy` declaration comment for why we send a key
+ [UpdateSnippetMutation, (...args) => mutateSpy('updateSnippet', ...args)],
+ [CreateSnippetMutation, (...args) => mutateSpy('createSnippet', ...args)],
+ ];
+ const apolloProvider = createMockApollo(requestHandlers);
+
+ wrapper = shallowMount(SnippetEditApp, {
+ apolloProvider,
+ localVue,
+ stubs: {
+ ApolloMutation,
+ FormFooterActions,
+ CaptchaModal: stubComponent(CaptchaModal),
+ },
+ provide: {
+ selectedLevel,
+ },
+ propsData: {
+ snippetGid: TEST_SNIPPET_GID,
+ markdownPreviewPath: 'http://preview.foo.bar',
+ markdownDocsPath: 'http://docs.foo.bar',
+ ...props,
+ },
+ });
};
- describe('rendering', () => {
- it('renders loader while the query is in flight', () => {
- createComponent({ loading: true });
+ // Creates comopnent and waits for gql load
+ const createComponentAndLoad = async (...args) => {
+ createComponent(...args);
+
+ await waitForPromises();
+ };
+
+ // Creates loaded component and submits form
+ const createComponentAndSubmit = async (...args) => {
+ await createComponentAndLoad(...args);
+
+ clickSubmitBtn();
+
+ await waitForPromises();
+ };
+
+ describe('when loading', () => {
+ it('renders loader', () => {
+ createComponent();
+
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
+ });
- it.each([[{}], [{ snippetGid: '' }]])(
- 'should render all required components with %s',
- (props) => {
- createComponent(props);
+ describe.each`
+ snippetGid | expectedQueries
+ ${TEST_SNIPPET_GID} | ${[[{ ids: [TEST_SNIPPET_GID] }]]}
+ ${''} | ${[]}
+ `('when loaded with snippetGid=$snippetGid', ({ snippetGid, expectedQueries }) => {
+ beforeEach(() => createComponentAndLoad({ props: { snippetGid } }));
- expect(wrapper.find(TitleField).exists()).toBe(true);
- expect(wrapper.find(SnippetDescriptionEdit).exists()).toBe(true);
- expect(wrapper.find(SnippetVisibilityEdit).exists()).toBe(true);
- expect(wrapper.find(FormFooterActions).exists()).toBe(true);
- expect(findBlobActions().exists()).toBe(true);
- },
- );
+ it(`queries with ${JSON.stringify(expectedQueries)}`, () => {
+ expect(getSpy.mock.calls).toEqual(expectedQueries);
+ });
+ it('should render components', () => {
+ expect(wrapper.find(CaptchaModal).exists()).toBe(true);
+ expect(wrapper.find(TitleField).exists()).toBe(true);
+ expect(wrapper.find(SnippetDescriptionEdit).exists()).toBe(true);
+ expect(wrapper.find(SnippetVisibilityEdit).exists()).toBe(true);
+ expect(wrapper.find(FormFooterActions).exists()).toBe(true);
+ expect(findBlobActions().exists()).toBe(true);
+ });
+
+ it('should hide loader', () => {
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ });
+ });
+
+ describe('default', () => {
it.each`
title | actions | shouldDisable
${''} | ${[]} | ${true}
@@ -211,163 +232,241 @@ describe('Snippet Edit app', () => {
${'foo'} | ${[TEST_ACTIONS.VALID, TEST_ACTIONS.NO_CONTENT]} | ${true}
${'foo'} | ${[TEST_ACTIONS.VALID, TEST_ACTIONS.NO_PATH]} | ${false}
`(
- 'should handle submit disable (title=$title, actions=$actions, shouldDisable=$shouldDisable)',
+ 'should handle submit disable (title="$title", actions="$actions", shouldDisable="$shouldDisable")',
async ({ title, actions, shouldDisable }) => {
- createComponent();
+ getSpy.mockResolvedValue(createQueryResponse({ title }));
+
+ await createComponentAndLoad();
- loadSnippet({ title });
triggerBlobActions(actions);
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(hasDisabledSubmit()).toBe(shouldDisable);
},
);
it.each`
- projectPath | snippetArg | expectation
- ${''} | ${[]} | ${urlUtils.joinPaths('/', relativeUrlRoot, '-', 'snippets')}
- ${'project/path'} | ${[]} | ${urlUtils.joinPaths('/', relativeUrlRoot, 'project/path/-', 'snippets')}
- ${''} | ${[createTestSnippet()]} | ${TEST_WEB_URL}
- ${'project/path'} | ${[createTestSnippet()]} | ${TEST_WEB_URL}
+ projectPath | snippetGid | expectation
+ ${''} | ${''} | ${urlUtils.joinPaths('/', relativeUrlRoot, '-', 'snippets')}
+ ${'project/path'} | ${''} | ${urlUtils.joinPaths('/', relativeUrlRoot, 'project/path/-', 'snippets')}
+ ${''} | ${TEST_SNIPPET_GID} | ${TEST_WEB_URL}
+ ${'project/path'} | ${TEST_SNIPPET_GID} | ${TEST_WEB_URL}
`(
- 'should set cancel href when (projectPath=$projectPath, snippet=$snippetArg)',
- async ({ projectPath, snippetArg, expectation }) => {
- createComponent({
- props: { projectPath },
+ 'should set cancel href (projectPath="$projectPath", snippetGid="$snippetGid")',
+ async ({ projectPath, snippetGid, expectation }) => {
+ await createComponentAndLoad({
+ props: {
+ projectPath,
+ snippetGid,
+ },
});
- loadSnippet(...snippetArg);
-
- await wrapper.vm.$nextTick();
-
expect(findCancelButton().attributes('href')).toBe(expectation);
},
);
- });
-
- describe('functionality', () => {
- it('does not fetch snippet when create a new snippet', async () => {
- createComponent({ props: { snippetGid: '' }, withApollo: true });
-
- jest.runOnlyPendingTimers();
- await wrapper.vm.$nextTick();
- expect(GetSnippetQuerySpy).not.toHaveBeenCalled();
- });
+ it.each([SNIPPET_VISIBILITY_PRIVATE, SNIPPET_VISIBILITY_INTERNAL, SNIPPET_VISIBILITY_PUBLIC])(
+ 'marks %s visibility by default',
+ async (visibility) => {
+ createComponent({
+ props: { snippetGid: '' },
+ selectedLevel: visibility,
+ });
- describe('default visibility', () => {
- it.each([SNIPPET_VISIBILITY_PRIVATE, SNIPPET_VISIBILITY_INTERNAL, SNIPPET_VISIBILITY_PUBLIC])(
- 'marks %s visibility by default',
- async (visibility) => {
- createComponent({
- props: { snippetGid: '' },
- selectedLevel: visibility,
- });
- expect(wrapper.vm.snippet.visibilityLevel).toEqual(visibility);
- },
- );
- });
+ expect(wrapper.find(SnippetVisibilityEdit).props('value')).toBe(visibility);
+ },
+ );
describe('form submission handling', () => {
it.each`
- snippetArg | projectPath | uploadedFiles | input | mutation
- ${[]} | ${'project/path'} | ${[]} | ${{ ...getApiData(), projectPath: 'project/path', uploadedFiles: [] }} | ${CreateSnippetMutation}
- ${[]} | ${''} | ${[]} | ${{ ...getApiData(), projectPath: '', uploadedFiles: [] }} | ${CreateSnippetMutation}
- ${[]} | ${''} | ${TEST_UPLOADED_FILES} | ${{ ...getApiData(), projectPath: '', uploadedFiles: TEST_UPLOADED_FILES }} | ${CreateSnippetMutation}
- ${[createTestSnippet()]} | ${'project/path'} | ${[]} | ${getApiData(createTestSnippet())} | ${UpdateSnippetMutation}
- ${[createTestSnippet()]} | ${''} | ${[]} | ${getApiData(createTestSnippet())} | ${UpdateSnippetMutation}
+ snippetGid | projectPath | uploadedFiles | input | mutationType
+ ${''} | ${'project/path'} | ${[]} | ${{ ...getApiData(), projectPath: 'project/path', uploadedFiles: [] }} | ${'createSnippet'}
+ ${''} | ${''} | ${[]} | ${{ ...getApiData(), projectPath: '', uploadedFiles: [] }} | ${'createSnippet'}
+ ${''} | ${''} | ${TEST_UPLOADED_FILES} | ${{ ...getApiData(), projectPath: '', uploadedFiles: TEST_UPLOADED_FILES }} | ${'createSnippet'}
+ ${TEST_SNIPPET_GID} | ${'project/path'} | ${[]} | ${getApiData(createSnippet())} | ${'updateSnippet'}
+ ${TEST_SNIPPET_GID} | ${''} | ${[]} | ${getApiData(createSnippet())} | ${'updateSnippet'}
`(
- 'should submit mutation with (snippet=$snippetArg, projectPath=$projectPath, uploadedFiles=$uploadedFiles)',
- async ({ snippetArg, projectPath, uploadedFiles, mutation, input }) => {
- createComponent({
+ 'should submit mutation $mutationType (snippetGid=$snippetGid, projectPath=$projectPath, uploadedFiles=$uploadedFiles)',
+ async ({ snippetGid, projectPath, uploadedFiles, mutationType, input }) => {
+ await createComponentAndLoad({
props: {
+ snippetGid,
projectPath,
},
});
- loadSnippet(...snippetArg);
+
setUploadFilesHtml(uploadedFiles);
- await wrapper.vm.$nextTick();
+ await nextTick();
clickSubmitBtn();
- expect(mutationTypes.RESOLVE).toHaveBeenCalledWith({
- mutation,
- variables: {
- input,
- },
+ expect(mutateSpy).toHaveBeenCalledTimes(1);
+ expect(mutateSpy).toHaveBeenCalledWith(mutationType, {
+ input,
});
},
);
it('should redirect to snippet view on successful mutation', async () => {
- createComponent();
- loadSnippet(createTestSnippet());
-
- clickSubmitBtn();
-
- await waitForPromises();
+ await createComponentAndSubmit();
expect(urlUtils.redirectTo).toHaveBeenCalledWith(TEST_WEB_URL);
});
it.each`
- snippetArg | projectPath | mutationRes | expectMessage
- ${[]} | ${'project/path'} | ${mutationTypes.RESOLVE_WITH_ERRORS} | ${`Can't create snippet: ${TEST_MUTATION_ERROR}`}
- ${[]} | ${''} | ${mutationTypes.RESOLVE_WITH_ERRORS} | ${`Can't create snippet: ${TEST_MUTATION_ERROR}`}
- ${[]} | ${''} | ${mutationTypes.REJECT} | ${`Can't create snippet: ${TEST_API_ERROR}`}
- ${[createTestSnippet()]} | ${'project/path'} | ${mutationTypes.RESOLVE_WITH_ERRORS} | ${`Can't update snippet: ${TEST_MUTATION_ERROR}`}
- ${[createTestSnippet()]} | ${''} | ${mutationTypes.RESOLVE_WITH_ERRORS} | ${`Can't update snippet: ${TEST_MUTATION_ERROR}`}
+ snippetGid | projectPath | mutationRes | expectMessage
+ ${''} | ${'project/path'} | ${createMutationResponseWithErrors('createSnippet')} | ${`Can't create snippet: ${TEST_MUTATION_ERROR}`}
+ ${''} | ${''} | ${createMutationResponseWithErrors('createSnippet')} | ${`Can't create snippet: ${TEST_MUTATION_ERROR}`}
+ ${TEST_SNIPPET_GID} | ${'project/path'} | ${createMutationResponseWithErrors('updateSnippet')} | ${`Can't update snippet: ${TEST_MUTATION_ERROR}`}
+ ${TEST_SNIPPET_GID} | ${''} | ${createMutationResponseWithErrors('updateSnippet')} | ${`Can't update snippet: ${TEST_MUTATION_ERROR}`}
`(
- 'should flash error with (snippet=$snippetArg, projectPath=$projectPath)',
- async ({ snippetArg, projectPath, mutationRes, expectMessage }) => {
- createComponent({
+ 'should flash error with (snippet=$snippetGid, projectPath=$projectPath)',
+ async ({ snippetGid, projectPath, mutationRes, expectMessage }) => {
+ mutateSpy.mockResolvedValue(mutationRes);
+
+ await createComponentAndSubmit({
props: {
projectPath,
+ snippetGid,
},
- mutationRes,
});
- loadSnippet(...snippetArg);
-
- clickSubmitBtn();
-
- await waitForPromises();
expect(urlUtils.redirectTo).not.toHaveBeenCalled();
expect(Flash).toHaveBeenCalledWith(expectMessage);
},
);
- });
- describe('on before unload', () => {
- it.each`
- condition | expectPrevented | action
- ${'there are no actions'} | ${false} | ${() => triggerBlobActions([])}
- ${'there are actions'} | ${true} | ${() => triggerBlobActions([testEntries.updated.diff])}
- ${'the snippet is being saved'} | ${false} | ${() => clickSubmitBtn()}
- `(
- 'handles before unload prevent when $condition (expectPrevented=$expectPrevented)',
- ({ expectPrevented, action }) => {
- createComponent();
- loadSnippet();
+ describe('with apollo network error', () => {
+ beforeEach(async () => {
+ jest.spyOn(console, 'error').mockImplementation();
+ mutateSpy.mockRejectedValue(TEST_API_ERROR);
- action();
+ await createComponentAndSubmit();
+ });
- const event = new Event('beforeunload');
- const returnValueSetter = jest.spyOn(event, 'returnValue', 'set');
+ it('should not redirect', () => {
+ expect(urlUtils.redirectTo).not.toHaveBeenCalled();
+ });
- window.dispatchEvent(event);
+ it('should flash', () => {
+ // Apollo automatically wraps the resolver's error in a NetworkError
+ expect(Flash).toHaveBeenCalledWith(
+ `Can't update snippet: Network error: ${TEST_API_ERROR.message}`,
+ );
+ });
- if (expectPrevented) {
- expect(returnValueSetter).toHaveBeenCalledWith(
- 'Are you sure you want to lose unsaved changes?',
- );
- } else {
- expect(returnValueSetter).not.toHaveBeenCalled();
- }
- },
- );
+ it('should console error', () => {
+ // eslint-disable-next-line no-console
+ expect(console.error).toHaveBeenCalledTimes(1);
+ // eslint-disable-next-line no-console
+ expect(console.error).toHaveBeenCalledWith(
+ '[gitlab] unexpected error while updating snippet',
+ expect.objectContaining({ message: `Network error: ${TEST_API_ERROR.message}` }),
+ );
+ });
+ });
+
+ describe('when needsCaptchaResponse is true', () => {
+ let modal;
+
+ beforeEach(async () => {
+ mutateSpy
+ .mockResolvedValueOnce(createMutationResponseWithRecaptcha('updateSnippet'))
+ .mockResolvedValueOnce(createMutationResponseWithErrors('updateSnippet'));
+
+ await createComponentAndSubmit();
+
+ modal = findCaptchaModal();
+
+ mutateSpy.mockClear();
+ });
+
+ it('should display captcha modal', () => {
+ expect(urlUtils.redirectTo).not.toHaveBeenCalled();
+ expect(modal.props()).toEqual({
+ needsCaptchaResponse: true,
+ captchaSiteKey: TEST_CAPTCHA_SITE_KEY,
+ });
+ });
+
+ describe.each`
+ response | expectedCalls
+ ${null} | ${[]}
+ ${TEST_CAPTCHA_RESPONSE} | ${[['updateSnippet', { input: { ...getApiData(createSnippet()), captchaResponse: TEST_CAPTCHA_RESPONSE } }]]}
+ `('when captcha response is $response', ({ response, expectedCalls }) => {
+ beforeEach(async () => {
+ modal.vm.$emit('receivedCaptchaResponse', response);
+
+ await nextTick();
+ });
+
+ it('sets needsCaptchaResponse to false', () => {
+ expect(modal.props('needsCaptchaResponse')).toEqual(false);
+ });
+
+ it(`expected to call times = ${expectedCalls.length}`, () => {
+ expect(mutateSpy.mock.calls).toEqual(expectedCalls);
+ });
+ });
+ });
});
});
+
+ describe('on before unload', () => {
+ it.each([
+ ['there are no actions', false, () => triggerBlobActions([])],
+ ['there is an empty action', false, () => triggerBlobActions([testEntries.empty.diff])],
+ ['there are actions', true, () => triggerBlobActions([testEntries.updated.diff])],
+ [
+ 'the title is set',
+ true,
+ () => {
+ triggerBlobActions([testEntries.empty.diff]);
+ setTitle('test');
+ },
+ ],
+ [
+ 'the description is set',
+ true,
+ () => {
+ triggerBlobActions([testEntries.empty.diff]);
+ setDescription('test');
+ },
+ ],
+ [
+ 'the snippet is being saved',
+ false,
+ () => {
+ triggerBlobActions([testEntries.updated.diff]);
+ clickSubmitBtn();
+ },
+ ],
+ ])(
+ 'handles before unload prevent when %s (expectPrevented=%s)',
+ async (_, expectPrevented, action) => {
+ await createComponentAndLoad({
+ props: {
+ snippetGid: '',
+ },
+ });
+
+ action();
+
+ const event = new Event('beforeunload');
+ const returnValueSetter = jest.spyOn(event, 'returnValue', 'set');
+
+ window.dispatchEvent(event);
+
+ if (expectPrevented) {
+ expect(returnValueSetter).toHaveBeenCalledWith(
+ 'Are you sure you want to lose unsaved changes?',
+ );
+ } else {
+ expect(returnValueSetter).not.toHaveBeenCalled();
+ }
+ },
+ );
+ });
});
diff --git a/spec/frontend/snippets/components/embed_dropdown_spec.js b/spec/frontend/snippets/components/embed_dropdown_spec.js
index f1eb7d43409..389b1c618a3 100644
--- a/spec/frontend/snippets/components/embed_dropdown_spec.js
+++ b/spec/frontend/snippets/components/embed_dropdown_spec.js
@@ -1,6 +1,6 @@
-import { escape as esc } from 'lodash';
-import { mount } from '@vue/test-utils';
import { GlFormInputGroup } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { escape as esc } from 'lodash';
import { TEST_HOST } from 'helpers/test_constants';
import EmbedDropdown from '~/snippets/components/embed_dropdown.vue';
diff --git a/spec/frontend/snippets/components/show_spec.js b/spec/frontend/snippets/components/show_spec.js
index b5ab7def753..e6162c6aad2 100644
--- a/spec/frontend/snippets/components/show_spec.js
+++ b/spec/frontend/snippets/components/show_spec.js
@@ -1,18 +1,17 @@
import { GlLoadingIcon } from '@gitlab/ui';
-import { Blob, BinaryBlob } from 'jest/blob/components/mock_data';
import { shallowMount } from '@vue/test-utils';
-import SnippetApp from '~/snippets/components/show.vue';
+import { Blob, BinaryBlob } from 'jest/blob/components/mock_data';
import EmbedDropdown from '~/snippets/components/embed_dropdown.vue';
+import SnippetApp from '~/snippets/components/show.vue';
+import SnippetBlob from '~/snippets/components/snippet_blob_view.vue';
import SnippetHeader from '~/snippets/components/snippet_header.vue';
import SnippetTitle from '~/snippets/components/snippet_title.vue';
-import SnippetBlob from '~/snippets/components/snippet_blob_view.vue';
-import CloneDropdownButton from '~/vue_shared/components/clone_dropdown.vue';
-
import {
SNIPPET_VISIBILITY_INTERNAL,
SNIPPET_VISIBILITY_PRIVATE,
SNIPPET_VISIBILITY_PUBLIC,
} from '~/snippets/constants';
+import CloneDropdownButton from '~/vue_shared/components/clone_dropdown.vue';
describe('Snippet view app', () => {
let wrapper;
diff --git a/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js b/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js
index 08056e788de..2693b26aeae 100644
--- a/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js
+++ b/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js
@@ -1,5 +1,5 @@
-import { times } from 'lodash';
import { shallowMount } from '@vue/test-utils';
+import { times } from 'lodash';
import SnippetBlobActionsEdit from '~/snippets/components/snippet_blob_actions_edit.vue';
import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue';
import {
diff --git a/spec/frontend/snippets/components/snippet_blob_edit_spec.js b/spec/frontend/snippets/components/snippet_blob_edit_spec.js
index 9d0311fd682..a7ab205ca7b 100644
--- a/spec/frontend/snippets/components/snippet_blob_edit_spec.js
+++ b/spec/frontend/snippets/components/snippet_blob_edit_spec.js
@@ -1,14 +1,14 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
-import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'helpers/test_constants';
-import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue';
+import waitForPromises from 'helpers/wait_for_promises';
import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue';
-import EditorLite from '~/vue_shared/components/editor_lite.vue';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue';
+import EditorLite from '~/vue_shared/components/editor_lite.vue';
jest.mock('~/flash');
diff --git a/spec/frontend/snippets/components/snippet_blob_view_spec.js b/spec/frontend/snippets/components/snippet_blob_view_spec.js
index 1ccecd7b5ba..b92c1907980 100644
--- a/spec/frontend/snippets/components/snippet_blob_view_spec.js
+++ b/spec/frontend/snippets/components/snippet_blob_view_spec.js
@@ -1,5 +1,5 @@
-import { nextTick } from 'vue';
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import {
Blob as BlobMock,
SimpleViewerMock,
@@ -7,16 +7,16 @@ import {
RichBlobContentMock,
SimpleBlobContentMock,
} from 'jest/blob/components/mock_data';
-import SnippetBlobView from '~/snippets/components/snippet_blob_view.vue';
-import BlobHeader from '~/blob/components/blob_header.vue';
import BlobContent from '~/blob/components/blob_content.vue';
+import BlobHeader from '~/blob/components/blob_header.vue';
import {
BLOB_RENDER_EVENT_LOAD,
BLOB_RENDER_EVENT_SHOW_SOURCE,
BLOB_RENDER_ERRORS,
} from '~/blob/components/constants';
-import { RichViewer, SimpleViewer } from '~/vue_shared/components/blob_viewers';
+import SnippetBlobView from '~/snippets/components/snippet_blob_view.vue';
import { SNIPPET_VISIBILITY_PUBLIC } from '~/snippets/constants';
+import { RichViewer, SimpleViewer } from '~/vue_shared/components/blob_viewers';
describe('Blob Embeddable', () => {
let wrapper;
diff --git a/spec/frontend/snippets/components/snippet_header_spec.js b/spec/frontend/snippets/components/snippet_header_spec.js
index 93a66db32c6..585614a6b79 100644
--- a/spec/frontend/snippets/components/snippet_header_spec.js
+++ b/spec/frontend/snippets/components/snippet_header_spec.js
@@ -1,11 +1,11 @@
-import { ApolloMutation } from 'vue-apollo';
import { GlButton, GlModal } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import { Blob, BinaryBlob } from 'jest/blob/components/mock_data';
+import { ApolloMutation } from 'vue-apollo';
import waitForPromises from 'helpers/wait_for_promises';
-import DeleteSnippetMutation from '~/snippets/mutations/deleteSnippet.mutation.graphql';
-import SnippetHeader from '~/snippets/components/snippet_header.vue';
+import { Blob, BinaryBlob } from 'jest/blob/components/mock_data';
import { differenceInMilliseconds } from '~/lib/utils/datetime_utility';
+import SnippetHeader from '~/snippets/components/snippet_header.vue';
+import DeleteSnippetMutation from '~/snippets/mutations/deleteSnippet.mutation.graphql';
describe('Snippet header component', () => {
let wrapper;
diff --git a/spec/frontend/snippets/components/snippet_title_spec.js b/spec/frontend/snippets/components/snippet_title_spec.js
index f201cfb19b7..48fb51ce703 100644
--- a/spec/frontend/snippets/components/snippet_title_spec.js
+++ b/spec/frontend/snippets/components/snippet_title_spec.js
@@ -1,7 +1,7 @@
import { GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import SnippetTitle from '~/snippets/components/snippet_title.vue';
import SnippetDescription from '~/snippets/components/snippet_description_view.vue';
+import SnippetTitle from '~/snippets/components/snippet_title.vue';
describe('Snippet header component', () => {
let wrapper;
diff --git a/spec/frontend/snippets/test_utils.js b/spec/frontend/snippets/test_utils.js
index 86262723157..8ba5a2fe5dc 100644
--- a/spec/frontend/snippets/test_utils.js
+++ b/spec/frontend/snippets/test_utils.js
@@ -1,3 +1,4 @@
+import { TEST_HOST } from 'helpers/test_constants';
import {
SNIPPET_BLOB_ACTION_CREATE,
SNIPPET_BLOB_ACTION_UPDATE,
@@ -8,6 +9,51 @@ import {
const CONTENT_1 = 'Lorem ipsum dolar\nSit amit\n\nGoodbye!\n';
const CONTENT_2 = 'Lorem ipsum dolar sit amit.\n\nGoodbye!\n';
+export const createGQLSnippet = () => ({
+ __typename: 'Snippet',
+ id: 7,
+ title: 'Snippet Title',
+ description: 'Lorem ipsum snippet desc',
+ descriptionHtml: '<p>Lorem ipsum snippet desc</p>',
+ createdAt: new Date(Date.now() - 1e6),
+ updatedAt: new Date(Date.now() - 1e3),
+ httpUrlToRepo: `${TEST_HOST}/repo`,
+ sshUrlToRepo: 'ssh://ssh.test/repo',
+ blobs: [],
+ userPermissions: {
+ __typename: 'SnippetPermissions',
+ adminSnippet: true,
+ updateSnippet: true,
+ },
+ project: {
+ __typename: 'Project',
+ fullPath: 'group/project',
+ webUrl: `${TEST_HOST}/group/project`,
+ },
+ author: {
+ __typename: 'User',
+ id: 1,
+ avatarUrl: `${TEST_HOST}/avatar.png`,
+ name: 'root',
+ username: 'root',
+ webUrl: `${TEST_HOST}/root`,
+ status: {
+ __typename: 'UserStatus',
+ emoji: '',
+ message: '',
+ },
+ },
+});
+
+export const createGQLSnippetsQueryResponse = (snippets) => ({
+ data: {
+ snippets: {
+ __typename: 'SnippetConnection',
+ nodes: snippets,
+ },
+ },
+});
+
export const testEntries = {
created: {
id: 'blob_1',
@@ -56,6 +102,15 @@ export const testEntries = {
content: CONTENT_2,
},
},
+ empty: {
+ id: 'empty',
+ diff: {
+ action: SNIPPET_BLOB_ACTION_CREATE,
+ filePath: '',
+ previousPath: '',
+ content: '',
+ },
+ },
};
export const createBlobFromTestEntry = ({ diff, origContent }, isOrig = false) => ({
diff --git a/spec/frontend/snippets/utils/error_spec.js b/spec/frontend/snippets/utils/error_spec.js
new file mode 100644
index 00000000000..385554568db
--- /dev/null
+++ b/spec/frontend/snippets/utils/error_spec.js
@@ -0,0 +1,16 @@
+import { getErrorMessage, UNEXPECTED_ERROR } from '~/snippets/utils/error';
+
+describe('~/snippets/utils/error', () => {
+ describe('getErrorMessage', () => {
+ it.each`
+ input | output
+ ${null} | ${UNEXPECTED_ERROR}
+ ${'message'} | ${'message'}
+ ${new Error('test message')} | ${'test message'}
+ ${{ networkError: 'Network error: test message' }} | ${'Network error: test message'}
+ ${{}} | ${UNEXPECTED_ERROR}
+ `('with $input, should return "$output"', ({ input, output }) => {
+ expect(getErrorMessage(input)).toBe(output);
+ });
+ });
+});
diff --git a/spec/frontend/static_site_editor/components/edit_area_spec.js b/spec/frontend/static_site_editor/components/edit_area_spec.js
index 07097d00cc5..17fb3fe788a 100644
--- a/spec/frontend/static_site_editor/components/edit_area_spec.js
+++ b/spec/frontend/static_site_editor/components/edit_area_spec.js
@@ -1,15 +1,14 @@
-import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { stubComponent } from 'helpers/stub_component';
-import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
-import { EDITOR_TYPES } from '~/vue_shared/components/rich_content_editor/constants';
-
import EditArea from '~/static_site_editor/components/edit_area.vue';
-import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue';
-import EditHeader from '~/static_site_editor/components/edit_header.vue';
import EditDrawer from '~/static_site_editor/components/edit_drawer.vue';
+import EditHeader from '~/static_site_editor/components/edit_header.vue';
+import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue';
import UnsavedChangesConfirmDialog from '~/static_site_editor/components/unsaved_changes_confirm_dialog.vue';
+import { EDITOR_TYPES } from '~/vue_shared/components/rich_content_editor/constants';
+import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
import {
sourceContentTitle as title,
diff --git a/spec/frontend/static_site_editor/components/edit_drawer_spec.js b/spec/frontend/static_site_editor/components/edit_drawer_spec.js
index c47eef59997..402dfe441c5 100644
--- a/spec/frontend/static_site_editor/components/edit_drawer_spec.js
+++ b/spec/frontend/static_site_editor/components/edit_drawer_spec.js
@@ -1,6 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
-
import { GlDrawer } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import EditDrawer from '~/static_site_editor/components/edit_drawer.vue';
import FrontMatterControls from '~/static_site_editor/components/front_matter_controls.vue';
diff --git a/spec/frontend/static_site_editor/components/edit_meta_controls_spec.js b/spec/frontend/static_site_editor/components/edit_meta_controls_spec.js
index cf82efc3d0b..7a8834933e0 100644
--- a/spec/frontend/static_site_editor/components/edit_meta_controls_spec.js
+++ b/spec/frontend/static_site_editor/components/edit_meta_controls_spec.js
@@ -1,6 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
-
import { GlDropdown, GlDropdownItem, GlFormInput, GlFormTextarea } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import EditMetaControls from '~/static_site_editor/components/edit_meta_controls.vue';
diff --git a/spec/frontend/static_site_editor/components/edit_meta_modal_spec.js b/spec/frontend/static_site_editor/components/edit_meta_modal_spec.js
index c7d0abee05c..3a336f6a230 100644
--- a/spec/frontend/static_site_editor/components/edit_meta_modal_spec.js
+++ b/spec/frontend/static_site_editor/components/edit_meta_modal_spec.js
@@ -1,12 +1,12 @@
-import { shallowMount } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui';
-import { useLocalStorageSpy } from 'helpers/local_storage_helper';
+import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import axios from '~/lib/utils/axios_utils';
-import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
-import EditMetaModal from '~/static_site_editor/components/edit_meta_modal.vue';
import EditMetaControls from '~/static_site_editor/components/edit_meta_controls.vue';
+import EditMetaModal from '~/static_site_editor/components/edit_meta_modal.vue';
import { MR_META_LOCAL_STORAGE_KEY } from '~/static_site_editor/constants';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import {
sourcePath,
mergeRequestMeta,
diff --git a/spec/frontend/static_site_editor/components/front_matter_controls_spec.js b/spec/frontend/static_site_editor/components/front_matter_controls_spec.js
index 8001f2fbd29..5fda3b40306 100644
--- a/spec/frontend/static_site_editor/components/front_matter_controls_spec.js
+++ b/spec/frontend/static_site_editor/components/front_matter_controls_spec.js
@@ -1,6 +1,6 @@
+import { GlFormGroup } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { GlFormGroup } from '@gitlab/ui';
import { humanize } from '~/lib/utils/text_utility';
import FrontMatterControls from '~/static_site_editor/components/front_matter_controls.vue';
diff --git a/spec/frontend/static_site_editor/components/submit_changes_error_spec.js b/spec/frontend/static_site_editor/components/submit_changes_error_spec.js
index 7af3014b338..82a5c5f624a 100644
--- a/spec/frontend/static_site_editor/components/submit_changes_error_spec.js
+++ b/spec/frontend/static_site_editor/components/submit_changes_error_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlButton, GlAlert } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import SubmitChangesError from '~/static_site_editor/components/submit_changes_error.vue';
diff --git a/spec/frontend/static_site_editor/graphql/resolvers/submit_content_changes_spec.js b/spec/frontend/static_site_editor/graphql/resolvers/submit_content_changes_spec.js
index 750b777cf5d..a0529f5f945 100644
--- a/spec/frontend/static_site_editor/graphql/resolvers/submit_content_changes_spec.js
+++ b/spec/frontend/static_site_editor/graphql/resolvers/submit_content_changes_spec.js
@@ -1,6 +1,6 @@
import savedContentMetaQuery from '~/static_site_editor/graphql/queries/saved_content_meta.query.graphql';
-import submitContentChanges from '~/static_site_editor/services/submit_content_changes';
import submitContentChangesResolver from '~/static_site_editor/graphql/resolvers/submit_content_changes';
+import submitContentChanges from '~/static_site_editor/services/submit_content_changes';
import {
projectId as project,
diff --git a/spec/frontend/static_site_editor/pages/home_spec.js b/spec/frontend/static_site_editor/pages/home_spec.js
index 3e488a950dc..0936ba3011c 100644
--- a/spec/frontend/static_site_editor/pages/home_spec.js
+++ b/spec/frontend/static_site_editor/pages/home_spec.js
@@ -1,15 +1,15 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
-import Home from '~/static_site_editor/pages/home.vue';
-import SkeletonLoader from '~/static_site_editor/components/skeleton_loader.vue';
import EditArea from '~/static_site_editor/components/edit_area.vue';
import EditMetaModal from '~/static_site_editor/components/edit_meta_modal.vue';
import InvalidContentMessage from '~/static_site_editor/components/invalid_content_message.vue';
+import SkeletonLoader from '~/static_site_editor/components/skeleton_loader.vue';
import SubmitChangesError from '~/static_site_editor/components/submit_changes_error.vue';
-import submitContentChangesMutation from '~/static_site_editor/graphql/mutations/submit_content_changes.mutation.graphql';
+import { TRACKING_ACTION_INITIALIZE_EDITOR } from '~/static_site_editor/constants';
import hasSubmittedChangesMutation from '~/static_site_editor/graphql/mutations/has_submitted_changes.mutation.graphql';
+import submitContentChangesMutation from '~/static_site_editor/graphql/mutations/submit_content_changes.mutation.graphql';
+import Home from '~/static_site_editor/pages/home.vue';
import { SUCCESS_ROUTE } from '~/static_site_editor/router/constants';
-import { TRACKING_ACTION_INITIALIZE_EDITOR } from '~/static_site_editor/constants';
import {
project,
diff --git a/spec/frontend/static_site_editor/pages/success_spec.js b/spec/frontend/static_site_editor/pages/success_spec.js
index 3fc69dc4586..fbdc2c435a0 100644
--- a/spec/frontend/static_site_editor/pages/success_spec.js
+++ b/spec/frontend/static_site_editor/pages/success_spec.js
@@ -1,8 +1,8 @@
-import { shallowMount } from '@vue/test-utils';
import { GlButton, GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import Success from '~/static_site_editor/pages/success.vue';
-import { savedContentMeta, returnUrl, sourcePath } from '../mock_data';
import { HOME_ROUTE } from '~/static_site_editor/router/constants';
+import { savedContentMeta, returnUrl, sourcePath } from '../mock_data';
describe('~/static_site_editor/pages/success.vue', () => {
const mergeRequestsIllustrationPath = 'illustrations/merge_requests.svg';
diff --git a/spec/frontend/static_site_editor/services/front_matterify_spec.js b/spec/frontend/static_site_editor/services/front_matterify_spec.js
index 866897f21ef..ec3752b30c6 100644
--- a/spec/frontend/static_site_editor/services/front_matterify_spec.js
+++ b/spec/frontend/static_site_editor/services/front_matterify_spec.js
@@ -1,3 +1,4 @@
+import { frontMatterify, stringify } from '~/static_site_editor/services/front_matterify';
import {
sourceContentYAML as content,
sourceContentHeaderObjYAML as yamlFrontMatterObj,
@@ -5,8 +6,6 @@ import {
sourceContentBody as body,
} from '../mock_data';
-import { frontMatterify, stringify } from '~/static_site_editor/services/front_matterify';
-
describe('static_site_editor/services/front_matterify', () => {
const frontMatterifiedContent = {
source: content,
diff --git a/spec/frontend/static_site_editor/services/parse_source_file_spec.js b/spec/frontend/static_site_editor/services/parse_source_file_spec.js
index ab9e63f4cd2..fdd11297e09 100644
--- a/spec/frontend/static_site_editor/services/parse_source_file_spec.js
+++ b/spec/frontend/static_site_editor/services/parse_source_file_spec.js
@@ -1,3 +1,4 @@
+import parseSourceFile from '~/static_site_editor/services/parse_source_file';
import {
sourceContentYAML as content,
sourceContentHeaderYAML as yamlFrontMatter,
@@ -5,8 +6,6 @@ import {
sourceContentBody as body,
} from '../mock_data';
-import parseSourceFile from '~/static_site_editor/services/parse_source_file';
-
describe('static_site_editor/services/parse_source_file', () => {
const contentComplex = [content, content, content].join('');
const complexBody = [body, content, content].join('');
diff --git a/spec/frontend/task_list_spec.js b/spec/frontend/task_list_spec.js
index a8bdc506102..b6ac3167fea 100644
--- a/spec/frontend/task_list_spec.js
+++ b/spec/frontend/task_list_spec.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
-import TaskList from '~/task_list';
import axios from '~/lib/utils/axios_utils';
+import TaskList from '~/task_list';
describe('TaskList', () => {
let taskList;
diff --git a/spec/frontend/terraform/components/states_table_actions_spec.js b/spec/frontend/terraform/components/states_table_actions_spec.js
index 3f5df8a96f8..61f6e9f0f7b 100644
--- a/spec/frontend/terraform/components/states_table_actions_spec.js
+++ b/spec/frontend/terraform/components/states_table_actions_spec.js
@@ -1,7 +1,8 @@
import { GlDropdown, GlModal, GlSprintf } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
-import createMockApollo from 'helpers/mock_apollo_helper';
import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import StateActions from '~/terraform/components/states_table_actions.vue';
import lockStateMutation from '~/terraform/graphql/mutations/lock_state.mutation.graphql';
import removeStateMutation from '~/terraform/graphql/mutations/remove_state.mutation.graphql';
@@ -13,7 +14,9 @@ localVue.use(VueApollo);
describe('StatesTableActions', () => {
let lockResponse;
let removeResponse;
+ let toast;
let unlockResponse;
+ let updateStateResponse;
let wrapper;
const defaultProps = {
@@ -26,7 +29,9 @@ describe('StatesTableActions', () => {
};
const createMockApolloProvider = () => {
- lockResponse = jest.fn().mockResolvedValue({ data: { terraformStateLock: { errors: [] } } });
+ lockResponse = jest
+ .fn()
+ .mockResolvedValue({ data: { terraformStateLock: { errors: ['There was an error'] } } });
removeResponse = jest
.fn()
@@ -36,26 +41,39 @@ describe('StatesTableActions', () => {
.fn()
.mockResolvedValue({ data: { terraformStateUnlock: { errors: [] } } });
- return createMockApollo([
- [lockStateMutation, lockResponse],
- [removeStateMutation, removeResponse],
- [unlockStateMutation, unlockResponse],
- ]);
+ updateStateResponse = jest.fn().mockResolvedValue({});
+
+ return createMockApollo(
+ [
+ [lockStateMutation, lockResponse],
+ [removeStateMutation, removeResponse],
+ [unlockStateMutation, unlockResponse],
+ ],
+ {
+ Mutation: {
+ addDataToTerraformState: updateStateResponse,
+ },
+ },
+ );
};
const createComponent = (propsData = defaultProps) => {
const apolloProvider = createMockApolloProvider();
+ toast = jest.fn();
+
wrapper = shallowMount(StateActions, {
apolloProvider,
localVue,
propsData,
+ mocks: { $toast: { show: toast } },
stubs: { GlDropdown, GlModal, GlSprintf },
});
return wrapper.vm.$nextTick();
};
+ const findActionsDropdown = () => wrapper.find(GlDropdown);
const findLockBtn = () => wrapper.find('[data-testid="terraform-state-lock"]');
const findUnlockBtn = () => wrapper.find('[data-testid="terraform-state-unlock"]');
const findDownloadBtn = () => wrapper.find('[data-testid="terraform-state-download"]');
@@ -69,10 +87,44 @@ describe('StatesTableActions', () => {
afterEach(() => {
lockResponse = null;
removeResponse = null;
+ toast = null;
unlockResponse = null;
+ updateStateResponse = null;
wrapper.destroy();
});
+ describe('when the state is loading', () => {
+ describe('when lock/unlock is processing', () => {
+ beforeEach(() => {
+ return createComponent({
+ state: {
+ ...defaultProps.state,
+ loadingLock: true,
+ },
+ });
+ });
+
+ it('disables the actions dropdown', () => {
+ expect(findActionsDropdown().props('disabled')).toBe(true);
+ });
+ });
+
+ describe('when remove is processing', () => {
+ beforeEach(() => {
+ return createComponent({
+ state: {
+ ...defaultProps.state,
+ loadingRemove: true,
+ },
+ });
+ });
+
+ it('disables the actions dropdown', () => {
+ expect(findActionsDropdown().props('disabled')).toBe(true);
+ });
+ });
+ });
+
describe('download button', () => {
it('displays a download button', () => {
expect(findDownloadBtn().text()).toBe('Download JSON');
@@ -104,7 +156,8 @@ describe('StatesTableActions', () => {
describe('when clicking the unlock button', () => {
beforeEach(() => {
findUnlockBtn().vm.$emit('click');
- return wrapper.vm.$nextTick();
+
+ return waitForPromises();
});
it('calls the unlock mutation', () => {
@@ -137,7 +190,8 @@ describe('StatesTableActions', () => {
describe('when clicking the lock button', () => {
beforeEach(() => {
findLockBtn().vm.$emit('click');
- return wrapper.vm.$nextTick();
+
+ return waitForPromises();
});
it('calls the lock mutation', () => {
@@ -145,6 +199,44 @@ describe('StatesTableActions', () => {
stateID: unlockedProps.state.id,
});
});
+
+ it('calls mutations to set loading and errors', () => {
+ // loading update
+ expect(updateStateResponse).toHaveBeenNthCalledWith(
+ 1,
+ {},
+ {
+ terraformState: {
+ ...unlockedProps.state,
+ _showDetails: false,
+ errorMessages: [],
+ loadingLock: true,
+ loadingRemove: false,
+ },
+ },
+ // Apollo fields
+ expect.any(Object),
+ expect.any(Object),
+ );
+
+ // final update
+ expect(updateStateResponse).toHaveBeenNthCalledWith(
+ 2,
+ {},
+ {
+ terraformState: {
+ ...unlockedProps.state,
+ _showDetails: true,
+ errorMessages: ['There was an error'],
+ loadingLock: false,
+ loadingRemove: false,
+ },
+ },
+ // Apollo fields
+ expect.any(Object),
+ expect.any(Object),
+ );
+ });
});
});
@@ -156,7 +248,7 @@ describe('StatesTableActions', () => {
describe('when clicking the remove button', () => {
beforeEach(() => {
findRemoveBtn().vm.$emit('click');
- return wrapper.vm.$nextTick();
+ return waitForPromises();
});
it('displays a remove modal', () => {
@@ -166,21 +258,70 @@ describe('StatesTableActions', () => {
});
describe('when submitting the remove modal', () => {
- it('does not call the remove mutation when state name is missing', async () => {
- findRemoveModal().vm.$emit('ok');
- await wrapper.vm.$nextTick();
+ describe('when state name is missing', () => {
+ beforeEach(() => {
+ findRemoveModal().vm.$emit('ok');
+ return waitForPromises();
+ });
- expect(removeResponse).not.toHaveBeenCalledWith();
+ it('does not call the remove mutation', () => {
+ expect(removeResponse).not.toHaveBeenCalledWith();
+ });
});
- it('calls the remove mutation when state name is present', async () => {
- await wrapper.setData({ removeConfirmText: defaultProps.state.name });
+ describe('when state name is present', () => {
+ beforeEach(async () => {
+ await wrapper.setData({ removeConfirmText: defaultProps.state.name });
+
+ findRemoveModal().vm.$emit('ok');
- findRemoveModal().vm.$emit('ok');
- await wrapper.vm.$nextTick();
+ await waitForPromises();
+ });
+
+ it('calls the remove mutation', () => {
+ expect(removeResponse).toHaveBeenCalledWith({ stateID: defaultProps.state.id });
+ });
+
+ it('calls the toast action', () => {
+ expect(toast).toHaveBeenCalledWith(`${defaultProps.state.name} successfully removed`);
+ });
- expect(removeResponse).toHaveBeenCalledWith({
- stateID: defaultProps.state.id,
+ it('calls mutations to set loading and errors', () => {
+ // loading update
+ expect(updateStateResponse).toHaveBeenNthCalledWith(
+ 1,
+ {},
+ {
+ terraformState: {
+ ...defaultProps.state,
+ _showDetails: false,
+ errorMessages: [],
+ loadingLock: false,
+ loadingRemove: true,
+ },
+ },
+ // Apollo fields
+ expect.any(Object),
+ expect.any(Object),
+ );
+
+ // final update
+ expect(updateStateResponse).toHaveBeenNthCalledWith(
+ 2,
+ {},
+ {
+ terraformState: {
+ ...defaultProps.state,
+ _showDetails: false,
+ errorMessages: [],
+ loadingLock: false,
+ loadingRemove: false,
+ },
+ },
+ // Apollo fields
+ expect.any(Object),
+ expect.any(Object),
+ );
});
});
});
diff --git a/spec/frontend/terraform/components/states_table_spec.js b/spec/frontend/terraform/components/states_table_spec.js
index f2b7bc00e5b..100e577f514 100644
--- a/spec/frontend/terraform/components/states_table_spec.js
+++ b/spec/frontend/terraform/components/states_table_spec.js
@@ -1,8 +1,8 @@
-import { GlIcon, GlTooltip } from '@gitlab/ui';
+import { GlIcon, GlLoadingIcon, GlTooltip } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { useFakeDate } from 'helpers/fake_date';
-import StateActions from '~/terraform/components/states_table_actions.vue';
import StatesTable from '~/terraform/components/states_table.vue';
+import StateActions from '~/terraform/components/states_table_actions.vue';
describe('StatesTable', () => {
let wrapper;
@@ -11,7 +11,11 @@ describe('StatesTable', () => {
const defaultProps = {
states: [
{
+ _showDetails: true,
+ errorMessages: ['State 1 has errored'],
name: 'state-1',
+ loadingLock: false,
+ loadingRemove: false,
lockedAt: '2020-10-13T00:00:00Z',
lockedByUser: {
name: 'user-1',
@@ -20,14 +24,22 @@ describe('StatesTable', () => {
latestVersion: null,
},
{
+ _showDetails: false,
+ errorMessages: [],
name: 'state-2',
+ loadingLock: true,
+ loadingRemove: false,
lockedAt: null,
lockedByUser: null,
updatedAt: '2020-10-10T00:00:00Z',
latestVersion: null,
},
{
+ _showDetails: false,
+ errorMessages: [],
name: 'state-3',
+ loadingLock: true,
+ loadingRemove: false,
lockedAt: '2020-10-10T00:00:00Z',
lockedByUser: {
name: 'user-2',
@@ -54,7 +66,11 @@ describe('StatesTable', () => {
},
},
{
+ _showDetails: true,
+ errorMessages: ['State 4 has errored'],
name: 'state-4',
+ loadingLock: false,
+ loadingRemove: false,
lockedAt: '2020-10-10T00:00:00Z',
lockedByUser: null,
updatedAt: '2020-10-10T00:00:00Z',
@@ -76,6 +92,17 @@ describe('StatesTable', () => {
},
},
},
+ {
+ _showDetails: false,
+ errorMessages: [],
+ name: 'state-5',
+ loadingLock: false,
+ loadingRemove: true,
+ lockedAt: null,
+ lockedByUser: null,
+ updatedAt: '2020-10-10T00:00:00Z',
+ latestVersion: null,
+ },
],
};
@@ -96,14 +123,15 @@ describe('StatesTable', () => {
});
it.each`
- name | toolTipText | locked | lineNumber
- ${'state-1'} | ${'Locked by user-1 2 days ago'} | ${true} | ${0}
- ${'state-2'} | ${null} | ${false} | ${1}
- ${'state-3'} | ${'Locked by user-2 5 days ago'} | ${true} | ${2}
- ${'state-4'} | ${'Locked by Unknown User 5 days ago'} | ${true} | ${3}
+ name | toolTipText | locked | loading | lineNumber
+ ${'state-1'} | ${'Locked by user-1 2 days ago'} | ${true} | ${false} | ${0}
+ ${'state-2'} | ${'Locking state'} | ${false} | ${true} | ${1}
+ ${'state-3'} | ${'Unlocking state'} | ${false} | ${true} | ${2}
+ ${'state-4'} | ${'Locked by Unknown User 5 days ago'} | ${true} | ${false} | ${3}
+ ${'state-5'} | ${'Removing'} | ${false} | ${true} | ${4}
`(
'displays the name and locked information "$name" for line "$lineNumber"',
- ({ name, toolTipText, locked, lineNumber }) => {
+ ({ name, toolTipText, locked, loading, lineNumber }) => {
const states = wrapper.findAll('[data-testid="terraform-states-table-name"]');
const state = states.at(lineNumber);
@@ -111,6 +139,7 @@ describe('StatesTable', () => {
expect(state.text()).toContain(name);
expect(state.find(GlIcon).exists()).toBe(locked);
+ expect(state.find(GlLoadingIcon).exists()).toBe(loading);
expect(toolTip.exists()).toBe(locked);
if (locked) {
@@ -154,6 +183,17 @@ describe('StatesTable', () => {
expect(findActions().length).toEqual(0);
});
+ it.each`
+ errorMessage | lineNumber
+ ${defaultProps.states[0].errorMessages[0]} | ${0}
+ ${defaultProps.states[3].errorMessages[0]} | ${1}
+ `('displays table error message "$errorMessage"', ({ errorMessage, lineNumber }) => {
+ const states = wrapper.findAll('[data-testid="terraform-states-table-error"]');
+ const state = states.at(lineNumber);
+
+ expect(state.text()).toBe(errorMessage);
+ });
+
describe('when user is a terraform administrator', () => {
beforeEach(() => {
return createComponent({
diff --git a/spec/frontend/terraform/components/terraform_list_spec.js b/spec/frontend/terraform/components/terraform_list_spec.js
index fb56a7135a3..882b7b55b3e 100644
--- a/spec/frontend/terraform/components/terraform_list_spec.js
+++ b/spec/frontend/terraform/components/terraform_list_spec.js
@@ -1,7 +1,8 @@
import { GlAlert, GlBadge, GlKeysetPagination, GlLoadingIcon, GlTab } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
-import createMockApollo from 'helpers/mock_apollo_helper';
import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import EmptyState from '~/terraform/components/empty_state.vue';
import StatesTable from '~/terraform/components/states_table.vue';
import TerraformList from '~/terraform/components/terraform_list.vue';
@@ -27,8 +28,20 @@ describe('TerraformList', () => {
},
};
+ const mockResolvers = {
+ TerraformState: {
+ _showDetails: jest.fn().mockResolvedValue(false),
+ errorMessages: jest.fn().mockResolvedValue([]),
+ loadingLock: jest.fn().mockResolvedValue(false),
+ loadingRemove: jest.fn().mockResolvedValue(false),
+ },
+ Mutation: {
+ addDataToTerraformState: jest.fn().mockResolvedValue({}),
+ },
+ };
+
const statsQueryResponse = queryResponse || jest.fn().mockResolvedValue(apolloQueryResponse);
- const apolloProvider = createMockApollo([[getStatesQuery, statsQueryResponse]]);
+ const apolloProvider = createMockApollo([[getStatesQuery, statsQueryResponse]], mockResolvers);
wrapper = shallowMount(TerraformList, {
localVue,
@@ -52,20 +65,28 @@ describe('TerraformList', () => {
describe('when there is a list of terraform states', () => {
const states = [
{
+ _showDetails: false,
+ errorMessages: [],
id: 'gid://gitlab/Terraform::State/1',
name: 'state-1',
+ latestVersion: null,
+ loadingLock: false,
+ loadingRemove: false,
lockedAt: null,
- updatedAt: null,
lockedByUser: null,
- latestVersion: null,
+ updatedAt: null,
},
{
+ _showDetails: false,
+ errorMessages: [],
id: 'gid://gitlab/Terraform::State/2',
name: 'state-2',
+ latestVersion: null,
+ loadingLock: false,
+ loadingRemove: false,
lockedAt: null,
- updatedAt: null,
lockedByUser: null,
- latestVersion: null,
+ updatedAt: null,
},
];
@@ -83,7 +104,7 @@ describe('TerraformList', () => {
},
});
- return wrapper.vm.$nextTick();
+ return waitForPromises();
});
it('displays a states tab and count', () => {
@@ -111,7 +132,7 @@ describe('TerraformList', () => {
},
});
- return wrapper.vm.$nextTick();
+ return waitForPromises();
});
it('renders the states table without pagination buttons', () => {
@@ -131,7 +152,7 @@ describe('TerraformList', () => {
},
});
- return wrapper.vm.$nextTick();
+ return waitForPromises();
});
it('displays a states tab with no count', () => {
@@ -149,7 +170,7 @@ describe('TerraformList', () => {
beforeEach(() => {
createWrapper({ terraformStates: null, queryResponse: jest.fn().mockRejectedValue() });
- return wrapper.vm.$nextTick();
+ return waitForPromises();
});
it('displays an alert message', () => {
diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js
index a122b06fdda..b6b29faef79 100644
--- a/spec/frontend/test_setup.js
+++ b/spec/frontend/test_setup.js
@@ -1,13 +1,13 @@
+import { config as testUtilsConfig } from '@vue/test-utils';
+import * as jqueryMatchers from 'custom-jquery-matchers';
import Vue from 'vue';
import 'jquery';
-
-import * as jqueryMatchers from 'custom-jquery-matchers';
-import { config as testUtilsConfig } from '@vue/test-utils';
+import { setGlobalDateToFakeDate } from 'helpers/fake_date';
import Translate from '~/vue_shared/translate';
-import { initializeTestTimeout } from './__helpers__/timeout';
import { getJSONFixture, loadHTMLFixture, setHTMLFixture } from './__helpers__/fixtures';
-import { setupManualMocks } from './mocks/mocks_helper';
+import { initializeTestTimeout } from './__helpers__/timeout';
import customMatchers from './matchers';
+import { setupManualMocks } from './mocks/mocks_helper';
import './__helpers__/dom_shims';
import './__helpers__/jquery';
@@ -20,6 +20,10 @@ process.on('unhandledRejection', global.promiseRejectionHandler);
setupManualMocks();
+// Fake the `Date` for the rest of the jest spec runtime environment.
+// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39496#note_503084332
+setGlobalDateToFakeDate();
+
afterEach(() =>
// give Promises a bit more time so they fail the right test
new Promise(setImmediate).then(() => {
diff --git a/spec/frontend/tooltips/components/tooltips_spec.js b/spec/frontend/tooltips/components/tooltips_spec.js
index 3a894427643..e21626456e2 100644
--- a/spec/frontend/tooltips/components/tooltips_spec.js
+++ b/spec/frontend/tooltips/components/tooltips_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlTooltip } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { useMockMutationObserver } from 'helpers/mock_dom_observer';
import Tooltips from '~/tooltips/components/tooltips.vue';
diff --git a/spec/frontend/tooltips/index_spec.js b/spec/frontend/tooltips/index_spec.js
index bff9ee0c7f2..9c03ca8f4c9 100644
--- a/spec/frontend/tooltips/index_spec.js
+++ b/spec/frontend/tooltips/index_spec.js
@@ -1,4 +1,3 @@
-import jQuery from 'jquery';
import {
add,
initTooltips,
@@ -146,29 +145,4 @@ describe('tooltips/index.js', () => {
expect(tooltipsApp.fixTitle).toHaveBeenCalledWith(target);
});
-
- describe('when glTooltipsEnabled feature flag is disabled', () => {
- beforeEach(() => {
- window.gon.features.glTooltips = false;
- });
-
- it.each`
- method | methodName | bootstrapParams
- ${dispose} | ${'dispose'} | ${'dispose'}
- ${fixTitle} | ${'fixTitle'} | ${'_fixTitle'}
- ${enable} | ${'enable'} | ${'enable'}
- ${disable} | ${'disable'} | ${'disable'}
- ${hide} | ${'hide'} | ${'hide'}
- ${show} | ${'show'} | ${'show'}
- ${add} | ${'init'} | ${{ title: 'the title' }}
- `('delegates $methodName to bootstrap tooltip API', ({ method, bootstrapParams }) => {
- const elements = jQuery(createTooltipTarget());
-
- jest.spyOn(jQuery.fn, 'tooltip');
-
- method(elements, bootstrapParams);
-
- expect(elements.tooltip).toHaveBeenCalledWith(bootstrapParams);
- });
- });
});
diff --git a/spec/frontend/user_lists/components/edit_user_list_spec.js b/spec/frontend/user_lists/components/edit_user_list_spec.js
index 958e86ac050..bd71a677a24 100644
--- a/spec/frontend/user_lists/components/edit_user_list_spec.js
+++ b/spec/frontend/user_lists/components/edit_user_list_spec.js
@@ -1,14 +1,14 @@
+import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
+import { createLocalVue, mount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
-import { createLocalVue, mount } from '@vue/test-utils';
-import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
-import createStore from '~/user_lists/store/edit';
+import { redirectTo } from '~/lib/utils/url_utility';
import EditUserList from '~/user_lists/components/edit_user_list.vue';
import UserListForm from '~/user_lists/components/user_list_form.vue';
+import createStore from '~/user_lists/store/edit';
import { userList } from '../../feature_flags/mock_data';
-import { redirectTo } from '~/lib/utils/url_utility';
jest.mock('~/api');
jest.mock('~/lib/utils/url_utility');
diff --git a/spec/frontend/user_lists/components/new_user_list_spec.js b/spec/frontend/user_lists/components/new_user_list_spec.js
index 421b49f40e9..a81e8912714 100644
--- a/spec/frontend/user_lists/components/new_user_list_spec.js
+++ b/spec/frontend/user_lists/components/new_user_list_spec.js
@@ -1,12 +1,12 @@
+import { GlAlert } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
-import { GlAlert } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
-import createStore from '~/user_lists/store/new';
-import NewUserList from '~/user_lists/components/new_user_list.vue';
import { redirectTo } from '~/lib/utils/url_utility';
+import NewUserList from '~/user_lists/components/new_user_list.vue';
+import createStore from '~/user_lists/store/new';
import { userList } from '../../feature_flags/mock_data';
jest.mock('~/api');
diff --git a/spec/frontend/user_lists/components/user_list_spec.js b/spec/frontend/user_lists/components/user_list_spec.js
index cd00c421154..f016b5091d9 100644
--- a/spec/frontend/user_lists/components/user_list_spec.js
+++ b/spec/frontend/user_lists/components/user_list_spec.js
@@ -1,12 +1,12 @@
-import Vue from 'vue';
-import Vuex from 'vuex';
+import { GlAlert, GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { uniq } from 'lodash';
-import { GlAlert, GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
+import Vue from 'vue';
+import Vuex from 'vuex';
import Api from '~/api';
-import { parseUserIds, stringifyUserIds } from '~/user_lists/store/utils';
-import createStore from '~/user_lists/store/show';
import UserList from '~/user_lists/components/user_list.vue';
+import createStore from '~/user_lists/store/show';
+import { parseUserIds, stringifyUserIds } from '~/user_lists/store/utils';
import { userList } from '../../feature_flags/mock_data';
jest.mock('~/api');
diff --git a/spec/frontend/user_lists/store/edit/actions_spec.js b/spec/frontend/user_lists/store/edit/actions_spec.js
index 7f0fb8e5401..c4b0f888d3e 100644
--- a/spec/frontend/user_lists/store/edit/actions_spec.js
+++ b/spec/frontend/user_lists/store/edit/actions_spec.js
@@ -1,9 +1,9 @@
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
-import createState from '~/user_lists/store/edit/state';
-import * as types from '~/user_lists/store/edit/mutation_types';
-import * as actions from '~/user_lists/store/edit/actions';
import { redirectTo } from '~/lib/utils/url_utility';
+import * as actions from '~/user_lists/store/edit/actions';
+import * as types from '~/user_lists/store/edit/mutation_types';
+import createState from '~/user_lists/store/edit/state';
import { userList } from '../../../feature_flags/mock_data';
jest.mock('~/api');
diff --git a/spec/frontend/user_lists/store/edit/mutations_spec.js b/spec/frontend/user_lists/store/edit/mutations_spec.js
index 3d4d2a59717..0943c64e934 100644
--- a/spec/frontend/user_lists/store/edit/mutations_spec.js
+++ b/spec/frontend/user_lists/store/edit/mutations_spec.js
@@ -1,7 +1,7 @@
import statuses from '~/user_lists/constants/edit';
-import createState from '~/user_lists/store/edit/state';
import * as types from '~/user_lists/store/edit/mutation_types';
import mutations from '~/user_lists/store/edit/mutations';
+import createState from '~/user_lists/store/edit/state';
import { userList } from '../../../feature_flags/mock_data';
describe('User List Edit Mutations', () => {
diff --git a/spec/frontend/user_lists/store/new/actions_spec.js b/spec/frontend/user_lists/store/new/actions_spec.js
index 9cc6212a125..916ec2e6da7 100644
--- a/spec/frontend/user_lists/store/new/actions_spec.js
+++ b/spec/frontend/user_lists/store/new/actions_spec.js
@@ -1,9 +1,9 @@
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
-import createState from '~/user_lists/store/new/state';
-import * as types from '~/user_lists/store/new/mutation_types';
-import * as actions from '~/user_lists/store/new/actions';
import { redirectTo } from '~/lib/utils/url_utility';
+import * as actions from '~/user_lists/store/new/actions';
+import * as types from '~/user_lists/store/new/mutation_types';
+import createState from '~/user_lists/store/new/state';
import { userList } from '../../../feature_flags/mock_data';
jest.mock('~/api');
diff --git a/spec/frontend/user_lists/store/new/mutations_spec.js b/spec/frontend/user_lists/store/new/mutations_spec.js
index 89e8a83eb25..a928849e941 100644
--- a/spec/frontend/user_lists/store/new/mutations_spec.js
+++ b/spec/frontend/user_lists/store/new/mutations_spec.js
@@ -1,6 +1,6 @@
-import createState from '~/user_lists/store/new/state';
import * as types from '~/user_lists/store/new/mutation_types';
import mutations from '~/user_lists/store/new/mutations';
+import createState from '~/user_lists/store/new/state';
describe('User List Edit Mutations', () => {
let state;
diff --git a/spec/frontend/user_lists/store/show/actions_spec.js b/spec/frontend/user_lists/store/show/actions_spec.js
index 25a6b9ec0e4..7b82fd8cf24 100644
--- a/spec/frontend/user_lists/store/show/actions_spec.js
+++ b/spec/frontend/user_lists/store/show/actions_spec.js
@@ -1,10 +1,10 @@
import testAction from 'helpers/vuex_action_helper';
import { userList } from 'jest/feature_flags/mock_data';
import Api from '~/api';
-import { stringifyUserIds } from '~/user_lists/store/utils';
-import createState from '~/user_lists/store/show/state';
-import * as types from '~/user_lists/store/show/mutation_types';
import * as actions from '~/user_lists/store/show/actions';
+import * as types from '~/user_lists/store/show/mutation_types';
+import createState from '~/user_lists/store/show/state';
+import { stringifyUserIds } from '~/user_lists/store/utils';
jest.mock('~/api');
diff --git a/spec/frontend/user_lists/store/show/mutations_spec.js b/spec/frontend/user_lists/store/show/mutations_spec.js
index cd379641ee1..92e2fcb06be 100644
--- a/spec/frontend/user_lists/store/show/mutations_spec.js
+++ b/spec/frontend/user_lists/store/show/mutations_spec.js
@@ -1,9 +1,9 @@
import { uniq } from 'lodash';
import { userList } from 'jest/feature_flags/mock_data';
-import createState from '~/user_lists/store/show/state';
-import mutations from '~/user_lists/store/show/mutations';
import { states } from '~/user_lists/constants/show';
import * as types from '~/user_lists/store/show/mutation_types';
+import mutations from '~/user_lists/store/show/mutations';
+import createState from '~/user_lists/store/show/state';
describe('User Lists Show Mutations', () => {
let mockState;
diff --git a/spec/frontend/user_popovers_spec.js b/spec/frontend/user_popovers_spec.js
index 1e0c4dd29ee..7c9c3d69efa 100644
--- a/spec/frontend/user_popovers_spec.js
+++ b/spec/frontend/user_popovers_spec.js
@@ -1,5 +1,5 @@
-import initUserPopovers from '~/user_popovers';
import UsersCache from '~/lib/utils/users_cache';
+import initUserPopovers from '~/user_popovers';
describe('User Popovers', () => {
const fixtureTemplate = 'merge_requests/merge_request_with_mentions.html';
diff --git a/spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js b/spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js
index fd8b0dddc61..d6a1c2d3b07 100644
--- a/spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js
+++ b/spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js
@@ -1,9 +1,9 @@
-import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import Approvals from '~/vue_merge_request_widget/components/approvals/approvals.vue';
import ApprovalsSummary from '~/vue_merge_request_widget/components/approvals/approvals_summary.vue';
import ApprovalsSummaryOptional from '~/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
import {
FETCH_LOADING,
FETCH_ERROR,
diff --git a/spec/frontend/vue_mr_widget/components/approvals/approvals_summary_optional_spec.js b/spec/frontend/vue_mr_widget/components/approvals/approvals_summary_optional_spec.js
index d9a5230f55f..65cafc647e0 100644
--- a/spec/frontend/vue_mr_widget/components/approvals/approvals_summary_optional_spec.js
+++ b/spec/frontend/vue_mr_widget/components/approvals/approvals_summary_optional_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import ApprovalsSummaryOptional from '~/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue';
const TEST_HELP_PATH = 'help/path';
diff --git a/spec/frontend/vue_mr_widget/components/approvals/approvals_summary_spec.js b/spec/frontend/vue_mr_widget/components/approvals/approvals_summary_spec.js
index b8ba619fbb1..ef712ec23a6 100644
--- a/spec/frontend/vue_mr_widget/components/approvals/approvals_summary_spec.js
+++ b/spec/frontend/vue_mr_widget/components/approvals/approvals_summary_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
-import { APPROVED_MESSAGE } from '~/vue_merge_request_widget/components/approvals/messages';
-import ApprovalsSummary from '~/vue_merge_request_widget/components/approvals/approvals_summary.vue';
import { toNounSeriesText } from '~/lib/utils/grammar';
+import ApprovalsSummary from '~/vue_merge_request_widget/components/approvals/approvals_summary.vue';
+import { APPROVED_MESSAGE } from '~/vue_merge_request_widget/components/approvals/messages';
import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue';
const testApprovers = () => Array.from({ length: 5 }, (_, i) => i).map((id) => ({ id }));
diff --git a/spec/frontend/vue_mr_widget/components/artifacts_list_app_spec.js b/spec/frontend/vue_mr_widget/components/artifacts_list_app_spec.js
index 2e1e21299b3..b2cc7d9be6b 100644
--- a/spec/frontend/vue_mr_widget/components/artifacts_list_app_spec.js
+++ b/spec/frontend/vue_mr_widget/components/artifacts_list_app_spec.js
@@ -1,8 +1,8 @@
+import { GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import Vuex from 'vuex';
-import Vue, { nextTick } from 'vue';
import MockAdapter from 'axios-mock-adapter';
-import { GlLoadingIcon } from '@gitlab/ui';
+import Vue, { nextTick } from 'vue';
+import Vuex from 'vuex';
import { TEST_HOST as FAKE_ENDPOINT } from 'helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
import ArtifactsListApp from '~/vue_merge_request_widget/components/artifacts_list_app.vue';
diff --git a/spec/frontend/vue_mr_widget/components/artifacts_list_spec.js b/spec/frontend/vue_mr_widget/components/artifacts_list_spec.js
index fd432381512..712abfe228a 100644
--- a/spec/frontend/vue_mr_widget/components/artifacts_list_spec.js
+++ b/spec/frontend/vue_mr_widget/components/artifacts_list_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import ArtifactsList from '~/vue_merge_request_widget/components/artifacts_list.vue';
import { artifacts } from '../mock_data';
diff --git a/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js b/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js
index ba2a8ee0a41..94d4cccab5f 100644
--- a/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js
@@ -1,5 +1,5 @@
-import { mount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import MrCollapsibleSection from '~/vue_merge_request_widget/components/mr_collapsible_extension.vue';
describe('Merge Request Collapsible Extension', () => {
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_alert_message_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_alert_message_spec.js
index 720ce613b85..07e869a070f 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_alert_message_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_alert_message_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
import MrWidgetAlertMessage from '~/vue_merge_request_widget/components/mr_widget_alert_message.vue';
describe('MrWidgetAlertMessage', () => {
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_icon_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_icon_spec.js
index 4bcae904ddc..ebd10f31fa7 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_icon_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_icon_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import MrWidgetIcon from '~/vue_merge_request_widget/components/mr_widget_icon.vue';
const TEST_ICON = 'commit';
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_memory_usage_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_memory_usage_spec.js
index 79a0dd1e760..f55d313a719 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_memory_usage_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_memory_usage_spec.js
@@ -1,6 +1,6 @@
-import Vue from 'vue';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
+import Vue from 'vue';
import MemoryUsage from '~/vue_merge_request_widget/components/deployment/memory_usage.vue';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_container_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_container_spec.js
index 7ff8d9678fe..3baade5161e 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_container_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_container_spec.js
@@ -1,10 +1,10 @@
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import MrWidgetPipelineContainer from '~/vue_merge_request_widget/components/mr_widget_pipeline_container.vue';
-import MrWidgetPipeline from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
+import axios from '~/lib/utils/axios_utils';
import ArtifactsApp from '~/vue_merge_request_widget/components/artifacts_list_app.vue';
+import MrWidgetPipeline from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
+import MrWidgetPipelineContainer from '~/vue_merge_request_widget/components/mr_widget_pipeline_container.vue';
import { mockStore } from '../mock_data';
-import axios from '~/lib/utils/axios_utils';
describe('MrWidgetPipelineContainer', () => {
let wrapper;
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_spec.js
index 3e5ab5cd32d..b93236d4628 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_spec.js
@@ -1,9 +1,9 @@
-import { shallowMount, mount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount, mount } from '@vue/test-utils';
import { trimText } from 'helpers/text_helper';
-import { SUCCESS } from '~/vue_merge_request_widget/constants';
-import PipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
import PipelineStage from '~/pipelines/components/pipelines_list/stage.vue';
+import PipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
+import { SUCCESS } from '~/vue_merge_request_widget/constants';
import mockData from '../mock_data';
describe('MRWidgetPipeline', () => {
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_rebase_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_rebase_spec.js
index bdd038edd71..5081e1e5906 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_rebase_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_rebase_spec.js
@@ -1,7 +1,7 @@
-import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
-import eventHub from '~/vue_merge_request_widget/event_hub';
+import { nextTick } from 'vue';
import WidgetRebase from '~/vue_merge_request_widget/components/states/mr_widget_rebase.vue';
+import eventHub from '~/vue_merge_request_widget/event_hub';
let wrapper;
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_status_icon_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_status_icon_spec.js
index 6c3b4a01659..c25e10c5249 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_status_icon_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_status_icon_spec.js
@@ -1,48 +1,60 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount, mount } from '@vue/test-utils';
import mrStatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
describe('MR widget status icon component', () => {
- let vm;
- let Component;
+ let wrapper;
- beforeEach(() => {
- Component = Vue.extend(mrStatusIcon);
- });
+ const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const findDisabledMergeButton = () => wrapper.find('[data-testid="disabled-merge-button"]');
+
+ const createWrapper = (props, mountFn = shallowMount) => {
+ wrapper = mountFn(mrStatusIcon, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
describe('while loading', () => {
it('renders loading icon', () => {
- vm = mountComponent(Component, { status: 'loading' });
+ createWrapper({ status: 'loading' });
- expect(vm.$el.querySelector('.mr-widget-icon span').classList).toContain('gl-spinner');
+ expect(findLoadingIcon().exists()).toBe(true);
});
});
describe('with status icon', () => {
- it('renders ci status icon', () => {
- vm = mountComponent(Component, { status: 'failed' });
+ it('renders success status icon', () => {
+ createWrapper({ status: 'success' }, mount);
+
+ expect(wrapper.find('[data-testid="status_success-icon"]').exists()).toBe(true);
+ });
+
+ it('renders failed status icon', () => {
+ createWrapper({ status: 'failed' }, mount);
- expect(vm.$el.querySelector('.js-ci-status-icon-failed')).not.toBeNull();
+ expect(wrapper.find('[data-testid="status_failed-icon"]').exists()).toBe(true);
});
});
describe('with disabled button', () => {
it('renders a disabled button', () => {
- vm = mountComponent(Component, { status: 'failed', showDisabledButton: true });
+ createWrapper({ status: 'failed', showDisabledButton: true });
- expect(vm.$el.querySelector('.js-disabled-merge-button').textContent.trim()).toEqual('Merge');
+ expect(findDisabledMergeButton().exists()).toBe(true);
});
});
describe('without disabled button', () => {
it('does not render a disabled button', () => {
- vm = mountComponent(Component, { status: 'failed' });
+ createWrapper({ status: 'failed' });
- expect(vm.$el.querySelector('.js-disabled-merge-button')).toBeNull();
+ expect(findDisabledMergeButton().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_suggest_pipeline_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_suggest_pipeline_spec.js
index 8fcc982ac99..b5afc1ab21a 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_suggest_pipeline_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_suggest_pipeline_spec.js
@@ -1,12 +1,10 @@
-import { mount, shallowMount } from '@vue/test-utils';
import { GlLink, GlSprintf } from '@gitlab/ui';
-import { mockTracking, triggerEvent, unmockTracking } from 'helpers/tracking_helper';
+import { mount, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import suggestPipelineComponent from '~/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue';
-import MrWidgetIcon from '~/vue_merge_request_widget/components/mr_widget_icon.vue';
-import dismissibleContainer from '~/vue_shared/components/dismissible_container.vue';
-import { suggestProps, iconName } from './pipeline_tour_mock_data';
+import { mockTracking, triggerEvent, unmockTracking } from 'helpers/tracking_helper';
import axios from '~/lib/utils/axios_utils';
+import MrWidgetIcon from '~/vue_merge_request_widget/components/mr_widget_icon.vue';
+import suggestPipelineComponent from '~/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue';
import {
SP_TRACK_LABEL,
SP_LINK_TRACK_EVENT,
@@ -15,6 +13,8 @@ import {
SP_SHOW_TRACK_VALUE,
SP_HELP_URL,
} from '~/vue_merge_request_widget/constants';
+import dismissibleContainer from '~/vue_shared/components/dismissible_container.vue';
+import { suggestProps, iconName } from './pipeline_tour_mock_data';
describe('MRWidgetSuggestPipeline', () => {
describe('template', () => {
diff --git a/spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_pipeline_failed_spec.js.snap b/spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_pipeline_failed_spec.js.snap
new file mode 100644
index 00000000000..a124008b36a
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_pipeline_failed_spec.js.snap
@@ -0,0 +1,24 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`PipelineFailed should render error message with a disabled merge button 1`] = `
+<div
+ class="mr-widget-body media"
+>
+ <status-icon-stub
+ showdisabledbutton="true"
+ status="warning"
+ />
+
+ <div
+ class="media-body space-children"
+ >
+ <span
+ class="bold"
+ >
+ <gl-sprintf-stub
+ message="The pipeline for this merge request did not complete. Push a new commit to fix the failure, or check the %{linkStart}troubleshooting documentation%{linkEnd} to see other possible actions."
+ />
+ </span>
+ </div>
+</div>
+`;
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js
index 850bbd93df5..4dd1bd2aa9c 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js
@@ -1,11 +1,11 @@
-import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { trimText } from 'helpers/text_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import autoMergeEnabledComponent from '~/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue';
-import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
-import eventHub from '~/vue_merge_request_widget/event_hub';
import { MWPS_MERGE_STRATEGY } from '~/vue_merge_request_widget/constants';
+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;
@@ -202,7 +202,11 @@ describe('MRWidgetAutoMergeEnabled', () => {
wrapper.vm.cancelAutomaticMerge();
setImmediate(() => {
expect(wrapper.vm.isCancellingAutoMerge).toBeTruthy();
- expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj);
+ if (mergeRequestWidgetGraphql) {
+ expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
+ } else {
+ expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj);
+ }
done();
});
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js
index dca3798f7ea..24198096564 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js
@@ -1,6 +1,6 @@
-import { nextTick } from 'vue';
-import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon, GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import AutoMergeFailedComponent from '~/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js
index 706d60368b5..4c763f40cbe 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlDropdownItem } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import CommitMessageDropdown from '~/vue_merge_request_widget/components/states/commit_message_dropdown.vue';
const commits = [
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
index f5a059698b6..d3fc1e0e05b 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
@@ -1,7 +1,7 @@
-import $ from 'jquery';
import { createLocalVue, shallowMount } from '@vue/test-utils';
-import { removeBreakLine } from 'helpers/text_helper';
+import $ from 'jquery';
import { TEST_HOST } from 'helpers/test_constants';
+import { removeBreakLine } from 'helpers/text_helper';
import ConflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts.vue';
describe('MRWidgetConflicts', () => {
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js
index 48c1a9eedf9..c1471314c4a 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js
@@ -54,7 +54,7 @@ describe('MRWidgetFailedToMerge', () => {
Vue.nextTick()
.then(() => {
- expect(vm.mergeError).toBe('contains line breaks');
+ expect(vm.mergeError).toBe('contains line breaks.');
})
.then(done)
.catch(done.fail);
@@ -113,14 +113,14 @@ describe('MRWidgetFailedToMerge', () => {
describe('while it is not regresing', () => {
it('renders warning icon and disabled merge button', () => {
expect(vm.$el.querySelector('.js-ci-status-icon-warning')).not.toBeNull();
- expect(vm.$el.querySelector('.js-disabled-merge-button').getAttribute('disabled')).toEqual(
- 'disabled',
- );
+ expect(
+ vm.$el.querySelector('[data-testid="disabled-merge-button"]').getAttribute('disabled'),
+ ).toEqual('disabled');
});
it('renders given error', () => {
expect(vm.$el.querySelector('.has-error-message').textContent.trim()).toEqual(
- 'Merge error happened',
+ 'Merge error happened.',
);
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js
index 36c4174c03d..6af8ac9e18e 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js
@@ -1,5 +1,8 @@
+import { getByRole } from '@testing-library/dom';
import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
+import { OPEN_REVERT_MODAL, OPEN_CHERRY_PICK_MODAL } from '~/projects/commit/constants';
+import modalEventHub from '~/projects/commit/event_hub';
import mergedComponent from '~/vue_merge_request_widget/components/states/mr_widget_merged.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
@@ -16,6 +19,7 @@ describe('MRWidgetMerged', () => {
};
beforeEach(() => {
+ jest.spyOn(document, 'dispatchEvent');
const Component = Vue.extend(mergedComponent);
const mr = {
isRemovingSourceBranch: false,
@@ -147,6 +151,26 @@ describe('MRWidgetMerged', () => {
});
});
+ it('calls dispatchDocumentEvent to load in the modal component', () => {
+ expect(document.dispatchEvent).toHaveBeenCalledWith(new CustomEvent('merged:UpdateActions'));
+ });
+
+ it('emits event to open the revert modal on revert button click', () => {
+ const eventHubSpy = jest.spyOn(modalEventHub, '$emit');
+
+ getByRole(vm.$el, 'button', { name: /Revert/i }).click();
+
+ expect(eventHubSpy).toHaveBeenCalledWith(OPEN_REVERT_MODAL);
+ });
+
+ it('emits event to open the cherry-pick modal on cherry-pick button click', () => {
+ const eventHubSpy = jest.spyOn(modalEventHub, '$emit');
+
+ getByRole(vm.$el, 'button', { name: /Cherry-pick/i }).click();
+
+ expect(eventHubSpy).toHaveBeenCalledWith(OPEN_CHERRY_PICK_MODAL);
+ });
+
it('has merged by information', () => {
expect(vm.$el.textContent).toContain('Merged by');
expect(vm.$el.textContent).toContain('Administrator');
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js
index 8847e4e6bdd..bd77a1d657e 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js
@@ -1,25 +1,27 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import { removeBreakLine } from 'helpers/text_helper';
-import pipelineBlockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue';
+import { shallowMount, mount } from '@vue/test-utils';
+import PipelineBlockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue';
describe('MRWidgetPipelineBlocked', () => {
- let vm;
- beforeEach(() => {
- const Component = Vue.extend(pipelineBlockedComponent);
- vm = mountComponent(Component);
- });
+ let wrapper;
+
+ const createWrapper = (mountFn = shallowMount) => {
+ wrapper = mountFn(PipelineBlockedComponent);
+ };
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
it('renders warning icon', () => {
- expect(vm.$el.querySelector('.ci-status-icon-warning')).not.toBe(null);
+ createWrapper(mount);
+
+ expect(wrapper.find('.ci-status-icon-warning').exists()).toBe(true);
});
it('renders information text', () => {
- expect(removeBreakLine(vm.$el.textContent).trim()).toContain(
+ createWrapper();
+
+ expect(wrapper.text()).toBe(
'Pipeline blocked. The pipeline for this merge request requires a manual action to proceed',
);
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js
index 179adef12d9..3e0840fef4e 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js
@@ -1,19 +1,30 @@
-import Vue from 'vue';
-import { removeBreakLine } from 'helpers/text_helper';
+import { shallowMount } from '@vue/test-utils';
+import statusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
import PipelineFailed from '~/vue_merge_request_widget/components/states/pipeline_failed.vue';
describe('PipelineFailed', () => {
- describe('template', () => {
- const Component = Vue.extend(PipelineFailed);
- const vm = new Component({
- el: document.createElement('div'),
- });
- it('should have correct elements', () => {
- expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy();
- expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy();
- expect(removeBreakLine(vm.$el.innerText).trim()).toContain(
- 'The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure',
- );
- });
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(PipelineFailed);
+ };
+
+ const findStatusIcon = () => wrapper.find(statusIcon);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('should render error message with a disabled merge button', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('merge button should be disabled', () => {
+ expect(findStatusIcon().props('showDisabledButton')).toBe(true);
});
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
index 8eddf59820c..983e4a35078 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -1,14 +1,14 @@
-import Vue from 'vue';
import { createLocalVue, shallowMount } from '@vue/test-utils';
-import ReadyToMerge from '~/vue_merge_request_widget/components/states/ready_to_merge.vue';
-import SquashBeforeMerge from '~/vue_merge_request_widget/components/states/squash_before_merge.vue';
-import CommitsHeader from '~/vue_merge_request_widget/components/states/commits_header.vue';
+import Vue from 'vue';
+import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
+import simplePoll from '~/lib/utils/simple_poll';
import CommitEdit from '~/vue_merge_request_widget/components/states/commit_edit.vue';
import CommitMessageDropdown from '~/vue_merge_request_widget/components/states/commit_message_dropdown.vue';
-import eventHub from '~/vue_merge_request_widget/event_hub';
+import CommitsHeader from '~/vue_merge_request_widget/components/states/commits_header.vue';
+import ReadyToMerge from '~/vue_merge_request_widget/components/states/ready_to_merge.vue';
+import SquashBeforeMerge from '~/vue_merge_request_widget/components/states/squash_before_merge.vue';
import { MWPS_MERGE_STRATEGY, MTWPS_MERGE_STRATEGY } from '~/vue_merge_request_widget/constants';
-import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
-import simplePoll from '~/lib/utils/simple_poll';
+import eventHub from '~/vue_merge_request_widget/event_hub';
jest.mock('~/lib/utils/simple_poll', () =>
jest.fn().mockImplementation(jest.requireActual('~/lib/utils/simple_poll').default),
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js
index 38920846a50..ef6a9b1e8fc 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
import { removeBreakLine } from 'helpers/text_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
import ShaMismatch from '~/vue_merge_request_widget/components/states/sha_mismatch.vue';
describe('ShaMismatch', () => {
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js
index cc160f6182d..8ead0002950 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js
@@ -1,5 +1,5 @@
-import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlFormCheckbox } from '@gitlab/ui';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
import SquashBeforeMerge from '~/vue_merge_request_widget/components/states/squash_before_merge.vue';
import { SQUASH_BEFORE_MERGE } from '~/vue_merge_request_widget/i18n';
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js
index a5531577a8c..6c0d69ea109 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js
@@ -1,7 +1,7 @@
import { mount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
-import UnresolvedDiscussions from '~/vue_merge_request_widget/components/states/unresolved_discussions.vue';
import notesEventHub from '~/notes/event_hub';
+import UnresolvedDiscussions from '~/vue_merge_request_widget/components/states/unresolved_discussions.vue';
function createComponent({ path = '' } = {}) {
return mount(UnresolvedDiscussions, {
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js
index 6d63d4b1be3..e0077a008a2 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import WorkInProgress from '~/vue_merge_request_widget/components/states/work_in_progress.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
jest.mock('~/flash');
diff --git a/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js b/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js
index 8da0d0f16d6..364f849eb4f 100644
--- a/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js
+++ b/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js
@@ -1,12 +1,12 @@
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import { invalidPlanWithName, plans, validPlanWithName } from './mock_data';
import axios from '~/lib/utils/axios_utils';
+import Poll from '~/lib/utils/poll';
import MrWidgetExpanableSection from '~/vue_merge_request_widget/components/mr_widget_expandable_section.vue';
import MrWidgetTerraformContainer from '~/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue';
-import Poll from '~/lib/utils/poll';
import TerraformPlan from '~/vue_merge_request_widget/components/terraform/terraform_plan.vue';
+import { invalidPlanWithName, plans, validPlanWithName } from './mock_data';
describe('MrWidgetTerraformConainer', () => {
let mock;
diff --git a/spec/frontend/vue_mr_widget/components/terraform/terraform_plan_spec.js b/spec/frontend/vue_mr_widget/components/terraform/terraform_plan_spec.js
index ea4eb44ebfe..f95a92c2cb1 100644
--- a/spec/frontend/vue_mr_widget/components/terraform/terraform_plan_spec.js
+++ b/spec/frontend/vue_mr_widget/components/terraform/terraform_plan_spec.js
@@ -33,7 +33,7 @@ describe('TerraformPlan', () => {
it('diplays the header text with a name', () => {
expect(wrapper.text()).toContain(
- `The Terraform report ${validPlanWithName.job_name} was generated in your pipelines.`,
+ `The report ${validPlanWithName.job_name} was generated in your pipelines.`,
);
});
@@ -55,7 +55,7 @@ describe('TerraformPlan', () => {
});
it('diplays the header text without a name', () => {
- expect(wrapper.text()).toContain('A Terraform report was generated in your pipelines.');
+ expect(wrapper.text()).toContain('A report was generated in your pipelines.');
});
});
@@ -70,7 +70,7 @@ describe('TerraformPlan', () => {
it('diplays the header text with a name', () => {
expect(wrapper.text()).toContain(
- `The Terraform report ${invalidPlanWithName.job_name} failed to generate.`,
+ `The report ${invalidPlanWithName.job_name} failed to generate.`,
);
});
@@ -85,7 +85,7 @@ describe('TerraformPlan', () => {
});
it('diplays the header text without a name', () => {
- expect(wrapper.text()).toContain('A Terraform report failed to generate.');
+ expect(wrapper.text()).toContain('A report failed to generate.');
});
it('does not render button because url is missing', () => {
diff --git a/spec/frontend/vue_mr_widget/deployment/deployment_action_button_spec.js b/spec/frontend/vue_mr_widget/deployment/deployment_action_button_spec.js
index bc0d2501809..8c5036e35f6 100644
--- a/spec/frontend/vue_mr_widget/deployment/deployment_action_button_spec.js
+++ b/spec/frontend/vue_mr_widget/deployment/deployment_action_button_spec.js
@@ -1,12 +1,12 @@
-import { mount } from '@vue/test-utils';
import { GlIcon, GlLoadingIcon, GlButton } from '@gitlab/ui';
-import DeploymentActionButton from '~/vue_merge_request_widget/components/deployment/deployment_action_button.vue';
+import { mount } from '@vue/test-utils';
import {
CREATED,
RUNNING,
DEPLOYING,
REDEPLOYING,
} from '~/vue_merge_request_widget/components/deployment/constants';
+import DeploymentActionButton from '~/vue_merge_request_widget/components/deployment/deployment_action_button.vue';
import { actionButtonMocks } from './deployment_mock_data';
const baseProps = {
diff --git a/spec/frontend/vue_mr_widget/deployment/deployment_actions_spec.js b/spec/frontend/vue_mr_widget/deployment/deployment_actions_spec.js
index 13c0665f929..22e58ac6abf 100644
--- a/spec/frontend/vue_mr_widget/deployment/deployment_actions_spec.js
+++ b/spec/frontend/vue_mr_widget/deployment/deployment_actions_spec.js
@@ -1,8 +1,6 @@
import { mount } from '@vue/test-utils';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
-import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
-import DeploymentActions from '~/vue_merge_request_widget/components/deployment/deployment_actions.vue';
import {
CREATED,
MANUAL_DEPLOY,
@@ -11,6 +9,8 @@ import {
REDEPLOYING,
STOPPING,
} from '~/vue_merge_request_widget/components/deployment/constants';
+import DeploymentActions from '~/vue_merge_request_widget/components/deployment/deployment_actions.vue';
+import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
import {
actionButtonMocks,
deploymentMockData,
diff --git a/spec/frontend/vue_mr_widget/deployment/deployment_spec.js b/spec/frontend/vue_mr_widget/deployment/deployment_spec.js
index 87bf32351bd..c27cbd8b781 100644
--- a/spec/frontend/vue_mr_widget/deployment/deployment_spec.js
+++ b/spec/frontend/vue_mr_widget/deployment/deployment_spec.js
@@ -1,7 +1,4 @@
import { mount } from '@vue/test-utils';
-import DeploymentComponent from '~/vue_merge_request_widget/components/deployment/deployment.vue';
-import DeploymentInfo from '~/vue_merge_request_widget/components/deployment/deployment_info.vue';
-import DeploymentViewButton from '~/vue_merge_request_widget/components/deployment/deployment_view_button.vue';
import {
CREATED,
RUNNING,
@@ -10,6 +7,9 @@ import {
CANCELED,
SKIPPED,
} from '~/vue_merge_request_widget/components/deployment/constants';
+import DeploymentComponent from '~/vue_merge_request_widget/components/deployment/deployment.vue';
+import DeploymentInfo from '~/vue_merge_request_widget/components/deployment/deployment_info.vue';
+import DeploymentViewButton from '~/vue_merge_request_widget/components/deployment/deployment_view_button.vue';
import { deploymentMockData, playDetails, retryDetails } from './deployment_mock_data';
describe('Deployment component', () => {
diff --git a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
index 1ea7fe1fbfe..7b020813bd5 100644
--- a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
@@ -1,28 +1,26 @@
import { mount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
import MockAdapter from 'axios-mock-adapter';
-import Api from '~/api';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { securityReportDownloadPathsQueryResponse } from 'jest/vue_shared/security_reports/mock_data';
import axios from '~/lib/utils/axios_utils';
-import MrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue';
-import eventHub from '~/vue_merge_request_widget/event_hub';
+import { setFaviconOverlay } from '~/lib/utils/favicon';
import notify from '~/lib/utils/notify';
import SmartInterval from '~/smart_interval';
-import { setFaviconOverlay } from '~/lib/utils/favicon';
+import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
+import eventHub from '~/vue_merge_request_widget/event_hub';
+import MrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue';
import { stateKey } from '~/vue_merge_request_widget/stores/state_maps';
-import mockData from './mock_data';
+import securityReportDownloadPathsQuery from '~/vue_shared/security_reports/queries/security_report_download_paths.query.graphql';
import { faviconDataUrl, overlayDataUrl } from '../lib/utils/mock_data';
-import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
+import mockData from './mock_data';
jest.mock('~/smart_interval');
jest.mock('~/lib/utils/favicon');
-const returnPromise = (data) =>
- new Promise((resolve) => {
- resolve({
- data,
- });
- });
+Vue.use(VueApollo);
describe('MrWidgetOptions', () => {
let wrapper;
@@ -48,7 +46,7 @@ describe('MrWidgetOptions', () => {
gon.features = {};
});
- const createComponent = (mrData = mockData) => {
+ const createComponent = (mrData = mockData, options = {}) => {
if (wrapper) {
wrapper.destroy();
}
@@ -57,6 +55,7 @@ describe('MrWidgetOptions', () => {
propsData: {
mrData: { ...mrData },
},
+ ...options,
});
return axios.waitForAll();
@@ -68,6 +67,7 @@ describe('MrWidgetOptions', () => {
describe('default', () => {
beforeEach(() => {
+ jest.spyOn(document, 'dispatchEvent');
return createComponent();
});
@@ -281,7 +281,7 @@ describe('MrWidgetOptions', () => {
let isCbExecuted;
beforeEach(() => {
- jest.spyOn(wrapper.vm.service, 'checkStatus').mockReturnValue(returnPromise(mockData));
+ jest.spyOn(wrapper.vm.service, 'checkStatus').mockResolvedValue({ data: mockData });
jest.spyOn(wrapper.vm.mr, 'setData').mockImplementation(() => {});
jest.spyOn(wrapper.vm, 'handleNotification').mockImplementation(() => {});
@@ -331,7 +331,7 @@ describe('MrWidgetOptions', () => {
it('should fetch deployments', () => {
jest
.spyOn(wrapper.vm.service, 'fetchDeployments')
- .mockReturnValue(returnPromise([{ id: 1, status: SUCCESS }]));
+ .mockResolvedValue({ data: [{ id: 1, status: SUCCESS }] });
wrapper.vm.fetchPreMergeDeployments();
@@ -347,13 +347,16 @@ describe('MrWidgetOptions', () => {
it('should fetch content of Cherry Pick and Revert modals', () => {
jest
.spyOn(wrapper.vm.service, 'fetchMergeActionsContent')
- .mockReturnValue(returnPromise('hello world'));
+ .mockResolvedValue({ data: 'hello world' });
wrapper.vm.fetchActionsContent();
return nextTick().then(() => {
expect(wrapper.vm.service.fetchMergeActionsContent).toHaveBeenCalled();
expect(document.body.textContent).toContain('hello world');
+ expect(document.dispatchEvent).toHaveBeenCalledWith(
+ new CustomEvent('merged:UpdateActions'),
+ );
});
});
});
@@ -822,36 +825,34 @@ describe('MrWidgetOptions', () => {
describe('security widget', () => {
describe.each`
- context | hasPipeline | reportType | isFlagEnabled | shouldRender
- ${'security report and flag enabled'} | ${true} | ${'sast'} | ${true} | ${true}
- ${'security report and flag disabled'} | ${true} | ${'sast'} | ${false} | ${false}
- ${'no security report and flag enabled'} | ${true} | ${'foo'} | ${true} | ${false}
- ${'no pipeline and flag enabled'} | ${false} | ${'sast'} | ${true} | ${false}
- `('given $context', ({ hasPipeline, reportType, isFlagEnabled, shouldRender }) => {
+ context | hasPipeline | shouldRender
+ ${'there is a pipeline'} | ${true} | ${true}
+ ${'no pipeline'} | ${false} | ${false}
+ `('given $context', ({ hasPipeline, shouldRender }) => {
beforeEach(() => {
- gon.features.coreSecurityMrWidget = isFlagEnabled;
+ const mrData = {
+ ...mockData,
+ ...(hasPipeline ? {} : { pipeline: null }),
+ };
- if (hasPipeline) {
- jest.spyOn(Api, 'pipelineJobs').mockResolvedValue({
- data: [{ artifacts: [{ file_type: reportType }] }],
- });
- }
+ // Override top-level mocked requests, which always use a fresh copy of
+ // mockData, which always includes the full pipeline object.
+ mock.onGet(mockData.merge_request_widget_path).reply(() => [200, mrData]);
+ mock.onGet(mockData.merge_request_cached_widget_path).reply(() => [200, mrData]);
- return createComponent({
- ...mockData,
- ...(hasPipeline ? {} : { pipeline: undefined }),
+ return createComponent(mrData, {
+ apolloProvider: createMockApollo([
+ [
+ securityReportDownloadPathsQuery,
+ async () => ({ data: securityReportDownloadPathsQueryResponse }),
+ ],
+ ]),
});
});
- if (shouldRender) {
- it('renders', () => {
- expect(findSecurityMrWidget().exists()).toBe(true);
- });
- } else {
- it('does not render', () => {
- expect(findSecurityMrWidget().exists()).toBe(false);
- });
- }
+ it(shouldRender ? 'renders' : 'does not render', () => {
+ expect(findSecurityMrWidget().exists()).toBe(shouldRender);
+ });
});
});
diff --git a/spec/frontend/vue_mr_widget/stores/artifacts_list/actions_spec.js b/spec/frontend/vue_mr_widget/stores/artifacts_list/actions_spec.js
index 1dfa7564535..9423fa17c44 100644
--- a/spec/frontend/vue_mr_widget/stores/artifacts_list/actions_spec.js
+++ b/spec/frontend/vue_mr_widget/stores/artifacts_list/actions_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
-import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'helpers/test_constants';
+import testAction from 'helpers/vuex_action_helper';
import axios from '~/lib/utils/axios_utils';
import {
setEndpoint,
@@ -11,8 +11,8 @@ import {
receiveArtifactsSuccess,
receiveArtifactsError,
} from '~/vue_merge_request_widget/stores/artifacts_list/actions';
-import state from '~/vue_merge_request_widget/stores/artifacts_list/state';
import * as types from '~/vue_merge_request_widget/stores/artifacts_list/mutation_types';
+import state from '~/vue_merge_request_widget/stores/artifacts_list/state';
describe('Artifacts App Store Actions', () => {
let mockedState;
diff --git a/spec/frontend/vue_mr_widget/stores/artifacts_list/mutations_spec.js b/spec/frontend/vue_mr_widget/stores/artifacts_list/mutations_spec.js
index ea89fdb72e9..a4e6788c7f6 100644
--- a/spec/frontend/vue_mr_widget/stores/artifacts_list/mutations_spec.js
+++ b/spec/frontend/vue_mr_widget/stores/artifacts_list/mutations_spec.js
@@ -1,6 +1,6 @@
-import state from '~/vue_merge_request_widget/stores/artifacts_list/state';
-import mutations from '~/vue_merge_request_widget/stores/artifacts_list/mutations';
import * as types from '~/vue_merge_request_widget/stores/artifacts_list/mutation_types';
+import mutations from '~/vue_merge_request_widget/stores/artifacts_list/mutations';
+import state from '~/vue_merge_request_widget/stores/artifacts_list/state';
describe('Artifacts Store Mutations', () => {
let stateCopy;
diff --git a/spec/frontend/alert_management/components/alert_details_spec.js b/spec/frontend/vue_shared/alert_details/alert_details_spec.js
index 976e50625a6..dd9a7be6268 100644
--- a/spec/frontend/alert_management/components/alert_details_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_details_spec.js
@@ -4,17 +4,14 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import AlertDetails from '~/alert_management/components/alert_details.vue';
-import AlertSummaryRow from '~/alert_management/components/alert_summary_row.vue';
-import {
- ALERTS_SEVERITY_LABELS,
- trackAlertsDetailsViewsOptions,
-} from '~/alert_management/constants';
-import createIssueMutation from '~/alert_management/graphql/mutations/create_issue_from_alert.mutation.graphql';
import { joinPaths } from '~/lib/utils/url_utility';
import Tracking from '~/tracking';
+import AlertDetails from '~/vue_shared/alert_details/components/alert_details.vue';
+import AlertSummaryRow from '~/vue_shared/alert_details/components/alert_summary_row.vue';
+import { SEVERITY_LEVELS } from '~/vue_shared/alert_details/constants';
+import createIssueMutation from '~/vue_shared/alert_details/graphql/mutations/alert_issue_create.mutation.graphql';
import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
-import mockAlerts from '../mocks/alerts.json';
+import mockAlerts from './mocks/alerts.json';
const mockAlert = mockAlerts[0];
const environmentName = 'Production';
@@ -29,7 +26,13 @@ describe('AlertDetails', () => {
const projectId = '1';
const $router = { replace: jest.fn() };
- function mountComponent({ data, loading = false, mountMethod = shallowMount, stubs = {} } = {}) {
+ function mountComponent({
+ data,
+ loading = false,
+ mountMethod = shallowMount,
+ provide = {},
+ stubs = {},
+ } = {}) {
wrapper = extendedWrapper(
mountMethod(AlertDetails, {
provide: {
@@ -37,6 +40,7 @@ describe('AlertDetails', () => {
projectPath,
projectIssuesPath,
projectId,
+ ...provide,
},
data() {
return {
@@ -86,6 +90,7 @@ describe('AlertDetails', () => {
const findEnvironmentName = () => wrapper.findByTestId('environmentName');
const findEnvironmentPath = () => wrapper.findByTestId('environmentPath');
const findDetailsTable = () => wrapper.find(AlertDetailsTable);
+ const findMetricsTab = () => wrapper.findByTestId('metrics');
describe('Alert details', () => {
describe('when alert is null', () => {
@@ -112,9 +117,7 @@ describe('AlertDetails', () => {
});
it('renders severity', () => {
- expect(wrapper.findByTestId('severity').text()).toBe(
- ALERTS_SEVERITY_LABELS[mockAlert.severity],
- );
+ expect(wrapper.findByTestId('severity').text()).toBe(SEVERITY_LEVELS[mockAlert.severity]);
});
it('renders a title', () => {
@@ -173,6 +176,15 @@ describe('AlertDetails', () => {
});
});
+ describe('Threat Monitoring details', () => {
+ it('should not render the metrics tab', () => {
+ mountComponent({
+ data: { alert: mockAlert, provide: { isThreatMonitoringPage: true } },
+ });
+ expect(findMetricsTab().exists()).toBe(false);
+ });
+ });
+
describe('Create incident from alert', () => {
it('should display "View incident" button that links the incident page when incident exists', () => {
const issueIid = '3';
@@ -321,16 +333,27 @@ describe('AlertDetails', () => {
});
describe('Snowplow tracking', () => {
+ const mountOptions = {
+ props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
+ data: { alert: mockAlert },
+ loading: false,
+ };
+
beforeEach(() => {
jest.spyOn(Tracking, 'event');
- mountComponent({
- props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
- data: { alert: mockAlert },
- loading: false,
- });
});
- it('should track alert details page views', () => {
+ it('should not track alert details page views when the tracking options do not exist', () => {
+ mountComponent(mountOptions);
+ expect(Tracking.event).not.toHaveBeenCalled();
+ });
+
+ it('should track alert details page views when the tracking options exist', () => {
+ const trackAlertsDetailsViewsOptions = {
+ category: 'Alert Management',
+ action: 'view_alert_details',
+ };
+ mountComponent({ ...mountOptions, provide: { trackAlertsDetailsViewsOptions } });
const { category, action } = trackAlertsDetailsViewsOptions;
expect(Tracking.event).toHaveBeenCalledWith(category, action);
});
diff --git a/spec/frontend/alert_management/components/alert_management_sidebar_todo_spec.js b/spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js
index ea7b4584a63..87ad5e36564 100644
--- a/spec/frontend/alert_management/components/alert_management_sidebar_todo_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js
@@ -1,8 +1,8 @@
import { mount } from '@vue/test-utils';
-import SidebarTodo from '~/alert_management/components/sidebar/sidebar_todo.vue';
-import createAlertTodoMutation from '~/alert_management/graphql/mutations/alert_todo_create.mutation.graphql';
import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql';
-import mockAlerts from '../mocks/alerts.json';
+import SidebarTodo from '~/vue_shared/alert_details/components/sidebar/sidebar_todo.vue';
+import createAlertTodoMutation from '~/vue_shared/alert_details/graphql/mutations/alert_todo_create.mutation.graphql';
+import mockAlerts from './mocks/alerts.json';
const mockAlert = mockAlerts[0];
@@ -59,7 +59,7 @@ describe('Alert Details Sidebar To Do', () => {
it('renders a button for adding a To-Do', async () => {
await wrapper.vm.$nextTick();
- expect(findToDoButton().text()).toBe('Add a To-Do');
+ expect(findToDoButton().text()).toBe('Add a to do');
});
it('calls `$apollo.mutate` with `createAlertTodoMutation` mutation and variables containing `iid`, `todoEvent`, & `projectPath`', async () => {
diff --git a/spec/frontend/alert_management/components/alert_metrics_spec.js b/spec/frontend/vue_shared/alert_details/alert_metrics_spec.js
index 42da8c3768b..b5a61a4adc1 100644
--- a/spec/frontend/alert_management/components/alert_metrics_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_metrics_spec.js
@@ -1,9 +1,9 @@
import { shallowMount } from '@vue/test-utils';
-import waitForPromises from 'helpers/wait_for_promises';
-import MockAdapter from 'axios-mock-adapter';
import axios from 'axios';
-import AlertMetrics from '~/alert_management/components/alert_metrics.vue';
+import MockAdapter from 'axios-mock-adapter';
+import waitForPromises from 'helpers/wait_for_promises';
import MetricEmbed from '~/monitoring/components/embeds/metric_embed.vue';
+import AlertMetrics from '~/vue_shared/alert_details/components/alert_metrics.vue';
jest.mock('~/monitoring/stores', () => ({
monitoringDashboard: {},
diff --git a/spec/frontend/alert_management/components/alert_status_spec.js b/spec/frontend/vue_shared/alert_details/alert_status_spec.js
index 6f2ddb86020..a866fc13539 100644
--- a/spec/frontend/alert_management/components/alert_status_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_status_spec.js
@@ -1,11 +1,10 @@
-import { shallowMount } from '@vue/test-utils';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
-import { trackAlertStatusUpdateOptions } from '~/alert_management/constants';
-import AlertManagementStatus from '~/alert_management/components/alert_status.vue';
-import updateAlertStatusMutation from '~/graphql_shared/mutations/update_alert_status.mutation.graphql';
+import updateAlertStatusMutation from '~/graphql_shared//mutations/alert_status_update.mutation.graphql';
import Tracking from '~/tracking';
-import mockAlerts from '../mocks/alerts.json';
+import AlertManagementStatus from '~/vue_shared/alert_details/components/alert_status.vue';
+import mockAlerts from './mocks/alerts.json';
const mockAlert = mockAlerts[0];
@@ -20,7 +19,7 @@ describe('AlertManagementStatus', () => {
return waitForPromises();
};
- function mountComponent({ props = {}, loading = false, stubs = {} } = {}) {
+ function mountComponent({ props = {}, provide = {}, loading = false, stubs = {} } = {}) {
wrapper = shallowMount(AlertManagementStatus, {
propsData: {
alert: { ...mockAlert },
@@ -28,6 +27,7 @@ describe('AlertManagementStatus', () => {
isSidebar: false,
...props,
},
+ provide,
mocks: {
$apollo: {
mutate: jest.fn(),
@@ -134,10 +134,25 @@ describe('AlertManagementStatus', () => {
describe('Snowplow tracking', () => {
beforeEach(() => {
jest.spyOn(Tracking, 'event');
+ });
+
+ it('should not track alert status updates when the tracking options do not exist', () => {
mountComponent({});
+ Tracking.event.mockClear();
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({});
+ findFirstStatusOption().vm.$emit('click');
+ setImmediate(() => {
+ expect(Tracking.event).not.toHaveBeenCalled();
+ });
});
- it('should track alert status updates', () => {
+ it('should track alert status updates when the tracking options exist', () => {
+ const trackAlertStatusUpdateOptions = {
+ category: 'Alert Management',
+ action: 'update_alert_status',
+ label: 'Status',
+ };
+ mountComponent({ provide: { trackAlertStatusUpdateOptions } });
Tracking.event.mockClear();
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({});
findFirstStatusOption().vm.$emit('click');
diff --git a/spec/frontend/alert_management/components/alert_summary_row_spec.js b/spec/frontend/vue_shared/alert_details/alert_summary_row_spec.js
index 47c715c089a..a2981478954 100644
--- a/spec/frontend/alert_management/components/alert_summary_row_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_summary_row_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import AlertSummaryRow from '~/alert_management/components/alert_summary_row.vue';
+import AlertSummaryRow from '~/vue_shared/alert_details/components/alert_summary_row.vue';
const label = 'a label';
const value = 'a value';
diff --git a/spec/frontend/alert_management/mocks/alerts.json b/spec/frontend/vue_shared/alert_details/mocks/alerts.json
index 5267a4fe50d..5267a4fe50d 100644
--- a/spec/frontend/alert_management/mocks/alerts.json
+++ b/spec/frontend/vue_shared/alert_details/mocks/alerts.json
diff --git a/spec/frontend/alert_management/components/sidebar/alert_managment_sidebar_assignees_spec.js b/spec/frontend/vue_shared/alert_details/sidebar/alert_managment_sidebar_assignees_spec.js
index 00c479071fe..28646994ed1 100644
--- a/spec/frontend/alert_management/components/sidebar/alert_managment_sidebar_assignees_spec.js
+++ b/spec/frontend/vue_shared/alert_details/sidebar/alert_managment_sidebar_assignees_spec.js
@@ -1,11 +1,11 @@
+import { GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
-import { GlDropdownItem } from '@gitlab/ui';
-import SidebarAssignee from '~/alert_management/components/sidebar/sidebar_assignee.vue';
-import SidebarAssignees from '~/alert_management/components/sidebar/sidebar_assignees.vue';
-import AlertSetAssignees from '~/alert_management/graphql/mutations/alert_set_assignees.mutation.graphql';
-import mockAlerts from '../../mocks/alerts.json';
+import SidebarAssignee from '~/vue_shared/alert_details/components/sidebar/sidebar_assignee.vue';
+import SidebarAssignees from '~/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue';
+import AlertSetAssignees from '~/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql';
+import mockAlerts from '../mocks/alerts.json';
const mockAlert = mockAlerts[0];
diff --git a/spec/frontend/alert_management/components/sidebar/alert_sidebar_spec.js b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_spec.js
index 5235ae63fee..70cf2597963 100644
--- a/spec/frontend/alert_management/components/sidebar/alert_sidebar_spec.js
+++ b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_spec.js
@@ -1,9 +1,10 @@
import { shallowMount, mount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
-import AlertSidebar from '~/alert_management/components/alert_sidebar.vue';
-import SidebarAssignees from '~/alert_management/components/sidebar/sidebar_assignees.vue';
-import mockAlerts from '../../mocks/alerts.json';
+import AlertSidebar from '~/vue_shared/alert_details/components/alert_sidebar.vue';
+import SidebarAssignees from '~/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue';
+import SidebarStatus from '~/vue_shared/alert_details/components/sidebar/sidebar_status.vue';
+import mockAlerts from '../mocks/alerts.json';
const mockAlert = mockAlerts[0];
@@ -11,7 +12,12 @@ describe('Alert Details Sidebar', () => {
let wrapper;
let mock;
- function mountComponent({ mountMethod = shallowMount, stubs = {}, alert = {} } = {}) {
+ function mountComponent({
+ mountMethod = shallowMount,
+ stubs = {},
+ alert = {},
+ provide = {},
+ } = {}) {
wrapper = mountMethod(AlertSidebar, {
data() {
return {
@@ -24,6 +30,7 @@ describe('Alert Details Sidebar', () => {
provide: {
projectPath: 'projectPath',
projectId: '1',
+ ...provide,
},
stubs,
mocks: {
@@ -60,5 +67,29 @@ describe('Alert Details Sidebar', () => {
});
expect(wrapper.find(SidebarAssignees).exists()).toBe(true);
});
+
+ it('should render side bar status dropdown', () => {
+ mountComponent({
+ mountMethod: mount,
+ alert: mockAlert,
+ });
+ expect(wrapper.find(SidebarStatus).exists()).toBe(true);
+ });
+ });
+
+ describe('the sidebar renders for threat monitoring', () => {
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mountComponent();
+ });
+
+ it('should not render side bar status dropdown', () => {
+ mountComponent({
+ mountMethod: mount,
+ alert: mockAlert,
+ provide: { isThreatMonitoringPage: true },
+ });
+ expect(wrapper.find(SidebarStatus).exists()).toBe(false);
+ });
});
});
diff --git a/spec/frontend/alert_management/components/sidebar/alert_sidebar_status_spec.js b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js
index 0b60a36cf54..f5b9efb4d98 100644
--- a/spec/frontend/alert_management/components/sidebar/alert_sidebar_status_spec.js
+++ b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js
@@ -1,10 +1,8 @@
-import { mount } from '@vue/test-utils';
import { GlDropdown, GlDropdownItem, GlLoadingIcon } from '@gitlab/ui';
-import { trackAlertStatusUpdateOptions } from '~/alert_management/constants';
-import AlertSidebarStatus from '~/alert_management/components/sidebar/sidebar_status.vue';
-import updateAlertStatusMutation from '~/graphql_shared/mutations/update_alert_status.mutation.graphql';
-import Tracking from '~/tracking';
-import mockAlerts from '../../mocks/alerts.json';
+import { mount } from '@vue/test-utils';
+import updateAlertStatusMutation from '~/graphql_shared/mutations/alert_status_update.mutation.graphql';
+import AlertSidebarStatus from '~/vue_shared/alert_details/components/sidebar/sidebar_status.vue';
+import mockAlerts from '../mocks/alerts.json';
const mockAlert = mockAlerts[0];
@@ -101,30 +99,5 @@ describe('Alert Details Sidebar Status', () => {
expect(wrapper.find('[data-testid="status"]').text()).toBe('Triggered');
});
});
-
- describe('Snowplow tracking', () => {
- beforeEach(() => {
- jest.spyOn(Tracking, 'event');
- mountComponent({
- props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
- data: { alert: mockAlert },
- loading: false,
- });
- });
-
- it('should track alert status updates', () => {
- Tracking.event.mockClear();
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({});
- findStatusDropdownItem().vm.$emit('click');
- const status = findStatusDropdownItem().text();
- setImmediate(() => {
- const { category, action, label } = trackAlertStatusUpdateOptions;
- expect(Tracking.event).toHaveBeenCalledWith(category, action, {
- label,
- property: status,
- });
- });
- });
- });
});
});
diff --git a/spec/frontend/alert_management/components/system_notes/alert_management_system_note_spec.js b/spec/frontend/vue_shared/alert_details/system_notes/alert_management_system_note_spec.js
index 65cfc600d76..a5a9fb55737 100644
--- a/spec/frontend/alert_management/components/system_notes/alert_management_system_note_spec.js
+++ b/spec/frontend/vue_shared/alert_details/system_notes/alert_management_system_note_spec.js
@@ -1,7 +1,7 @@
-import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
-import SystemNote from '~/alert_management/components/system_notes/system_note.vue';
-import mockAlerts from '../../mocks/alerts.json';
+import { shallowMount } from '@vue/test-utils';
+import SystemNote from '~/vue_shared/alert_details/components/system_notes/system_note.vue';
+import mockAlerts from '../mocks/alerts.json';
const mockAlert = mockAlerts[1];
diff --git a/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap
index 20ea897e29c..3be609f0dad 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap
@@ -18,13 +18,9 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
class="award-emoji-block"
data-testid="award-html"
>
-
-
<gl-emoji
data-name="thumbsup"
/>
-
-
</span>
<span
@@ -52,13 +48,9 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
class="award-emoji-block"
data-testid="award-html"
>
-
-
<gl-emoji
data-name="thumbsdown"
/>
-
-
</span>
<span
@@ -86,13 +78,9 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
class="award-emoji-block"
data-testid="award-html"
>
-
-
<gl-emoji
data-name="smile"
/>
-
-
</span>
<span
@@ -120,13 +108,9 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
class="award-emoji-block"
data-testid="award-html"
>
-
-
<gl-emoji
data-name="ok_hand"
/>
-
-
</span>
<span
@@ -154,13 +138,9 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
class="award-emoji-block"
data-testid="award-html"
>
-
-
<gl-emoji
data-name="cactus"
/>
-
-
</span>
<span
@@ -188,13 +168,9 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
class="award-emoji-block"
data-testid="award-html"
>
-
-
<gl-emoji
data-name="a"
/>
-
-
</span>
<span
@@ -222,13 +198,9 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
class="award-emoji-block"
data-testid="award-html"
>
-
-
<gl-emoji
data-name="b"
/>
-
-
</span>
<span
diff --git a/spec/frontend/vue_shared/components/actions_button_spec.js b/spec/frontend/vue_shared/components/actions_button_spec.js
index 2ac4bfda29a..e5b7b693cb5 100644
--- a/spec/frontend/vue_shared/components/actions_button_spec.js
+++ b/spec/frontend/vue_shared/components/actions_button_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlDropdown, GlDropdownDivider, GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import ActionsButton from '~/vue_shared/components/actions_button.vue';
diff --git a/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js b/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
index 22643a17b2b..f592db935ec 100644
--- a/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
+import { handleBlobRichViewer } from '~/blob/viewer';
import RichViewer from '~/vue_shared/components/blob_viewers/rich_viewer.vue';
import MarkdownFieldView from '~/vue_shared/components/markdown/field_view.vue';
-import { handleBlobRichViewer } from '~/blob/viewer';
jest.mock('~/blob/viewer');
diff --git a/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js b/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js
index 8434fdaccde..9a0616343fe 100644
--- a/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import SimpleViewer from '~/vue_shared/components/blob_viewers/simple_viewer.vue';
import { HIGHLIGHT_CLASS_NAME } from '~/vue_shared/components/blob_viewers/constants';
+import SimpleViewer from '~/vue_shared/components/blob_viewers/simple_viewer.vue';
describe('Blob Simple Viewer component', () => {
let wrapper;
diff --git a/spec/frontend/vue_shared/components/changed_file_icon_spec.js b/spec/frontend/vue_shared/components/changed_file_icon_spec.js
index 80918c5e771..6b9658a6d18 100644
--- a/spec/frontend/vue_shared/components/changed_file_icon_spec.js
+++ b/spec/frontend/vue_shared/components/changed_file_icon_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
const changedFile = () => ({ changed: true });
diff --git a/spec/frontend/vue_shared/components/ci_badge_link_spec.js b/spec/frontend/vue_shared/components/ci_badge_link_spec.js
index a633ef65aa4..a943d931f67 100644
--- a/spec/frontend/vue_shared/components/ci_badge_link_spec.js
+++ b/spec/frontend/vue_shared/components/ci_badge_link_spec.js
@@ -1,10 +1,9 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import ciBadge from '~/vue_shared/components/ci_badge_link.vue';
+import { shallowMount } from '@vue/test-utils';
+import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
describe('CI Badge Link Component', () => {
- let CIBadge;
- let vm;
+ let wrapper;
const statuses = {
canceled: {
@@ -72,29 +71,30 @@ describe('CI Badge Link Component', () => {
},
};
- beforeEach(() => {
- CIBadge = Vue.extend(ciBadge);
- });
+ const findIcon = () => wrapper.findComponent(CiIcon);
+
+ const createComponent = (propsData) => {
+ wrapper = shallowMount(CiBadge, { propsData });
+ };
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
+ wrapper = null;
});
- it('should render each status badge', () => {
- Object.keys(statuses).map((status) => {
- vm = mountComponent(CIBadge, { status: statuses[status] });
+ it.each(Object.keys(statuses))('should render badge for status: %s', (status) => {
+ createComponent({ status: statuses[status] });
- expect(vm.$el.getAttribute('href')).toEqual(statuses[status].details_path);
- expect(vm.$el.textContent.trim()).toEqual(statuses[status].text);
- expect(vm.$el.getAttribute('class')).toContain(`ci-status ci-${statuses[status].group}`);
- expect(vm.$el.querySelector('svg')).toBeDefined();
- return vm;
- });
+ expect(wrapper.attributes('href')).toBe(statuses[status].details_path);
+ expect(wrapper.text()).toBe(statuses[status].text);
+ expect(wrapper.classes()).toContain('ci-status');
+ expect(wrapper.classes()).toContain(`ci-${statuses[status].group}`);
+ expect(findIcon().exists()).toBe(true);
});
it('should not render label', () => {
- vm = mountComponent(CIBadge, { status: statuses.canceled, showText: false });
+ createComponent({ status: statuses.canceled, showText: false });
- expect(vm.$el.textContent.trim()).toEqual('');
+ expect(wrapper.text()).toBe('');
});
});
diff --git a/spec/frontend/vue_shared/components/ci_icon_spec.js b/spec/frontend/vue_shared/components/ci_icon_spec.js
index 63afe631063..6d52db7ae65 100644
--- a/spec/frontend/vue_shared/components/ci_icon_spec.js
+++ b/spec/frontend/vue_shared/components/ci_icon_spec.js
@@ -1,122 +1,51 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
+import { GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import ciIcon from '~/vue_shared/components/ci_icon.vue';
describe('CI Icon component', () => {
- const Component = Vue.extend(ciIcon);
- let vm;
+ let wrapper;
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
+ wrapper = null;
});
it('should render a span element with an svg', () => {
- vm = mountComponent(Component, {
- status: {
- icon: 'status_success',
- },
- });
-
- expect(vm.$el.tagName).toEqual('SPAN');
- expect(vm.$el.querySelector('span > svg')).toBeDefined();
- });
-
- it('should render a success status', () => {
- vm = mountComponent(Component, {
- status: {
- icon: 'status_success',
- group: 'success',
- },
- });
-
- expect(vm.$el.classList.contains('ci-status-icon-success')).toEqual(true);
- });
-
- it('should render a failed status', () => {
- vm = mountComponent(Component, {
- status: {
- icon: 'status_failed',
- group: 'failed',
- },
- });
-
- expect(vm.$el.classList.contains('ci-status-icon-failed')).toEqual(true);
- });
-
- it('should render success with warnings status', () => {
- vm = mountComponent(Component, {
- status: {
- icon: 'status_warning',
- group: 'warning',
- },
- });
-
- expect(vm.$el.classList.contains('ci-status-icon-warning')).toEqual(true);
- });
-
- it('should render pending status', () => {
- vm = mountComponent(Component, {
- status: {
- icon: 'status_pending',
- group: 'pending',
- },
+ wrapper = shallowMount(ciIcon, {
+ propsData: {
+ status: {
+ icon: 'status_success',
+ },
+ },
+ });
+
+ expect(wrapper.find('span').exists()).toBe(true);
+ expect(wrapper.find(GlIcon).exists()).toBe(true);
+ });
+
+ describe('rendering a status', () => {
+ it.each`
+ icon | group | cssClass
+ ${'status_success'} | ${'success'} | ${'ci-status-icon-success'}
+ ${'status_failed'} | ${'failed'} | ${'ci-status-icon-failed'}
+ ${'status_warning'} | ${'warning'} | ${'ci-status-icon-warning'}
+ ${'status_pending'} | ${'pending'} | ${'ci-status-icon-pending'}
+ ${'status_running'} | ${'running'} | ${'ci-status-icon-running'}
+ ${'status_created'} | ${'created'} | ${'ci-status-icon-created'}
+ ${'status_skipped'} | ${'skipped'} | ${'ci-status-icon-skipped'}
+ ${'status_canceled'} | ${'canceled'} | ${'ci-status-icon-canceled'}
+ ${'status_manual'} | ${'manual'} | ${'ci-status-icon-manual'}
+ `('should render a $group status', ({ icon, group, cssClass }) => {
+ wrapper = shallowMount(ciIcon, {
+ propsData: {
+ status: {
+ icon,
+ group,
+ },
+ },
+ });
+
+ expect(wrapper.classes()).toContain(cssClass);
});
-
- expect(vm.$el.classList.contains('ci-status-icon-pending')).toEqual(true);
- });
-
- it('should render running status', () => {
- vm = mountComponent(Component, {
- status: {
- icon: 'status_running',
- group: 'running',
- },
- });
-
- expect(vm.$el.classList.contains('ci-status-icon-running')).toEqual(true);
- });
-
- it('should render created status', () => {
- vm = mountComponent(Component, {
- status: {
- icon: 'status_created',
- group: 'created',
- },
- });
-
- expect(vm.$el.classList.contains('ci-status-icon-created')).toEqual(true);
- });
-
- it('should render skipped status', () => {
- vm = mountComponent(Component, {
- status: {
- icon: 'status_skipped',
- group: 'skipped',
- },
- });
-
- expect(vm.$el.classList.contains('ci-status-icon-skipped')).toEqual(true);
- });
-
- it('should render canceled status', () => {
- vm = mountComponent(Component, {
- status: {
- icon: 'status_canceled',
- group: 'canceled',
- },
- });
-
- expect(vm.$el.classList.contains('ci-status-icon-canceled')).toEqual(true);
- });
-
- it('should render status for manual action', () => {
- vm = mountComponent(Component, {
- status: {
- icon: 'status_manual',
- group: 'manual',
- },
- });
-
- expect(vm.$el.classList.contains('ci-status-icon-manual')).toEqual(true);
});
});
diff --git a/spec/frontend/vue_shared/components/clipboard_button_spec.js b/spec/frontend/vue_shared/components/clipboard_button_spec.js
index 0d4266ce82f..ab4008484e5 100644
--- a/spec/frontend/vue_shared/components/clipboard_button_spec.js
+++ b/spec/frontend/vue_shared/components/clipboard_button_spec.js
@@ -1,7 +1,7 @@
-import { mount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import { mount } from '@vue/test-utils';
import initCopyToClipboard from '~/behaviors/copy_to_clipboard';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
describe('clipboard button', () => {
let wrapper;
diff --git a/spec/frontend/vue_shared/components/clone_dropdown_spec.js b/spec/frontend/vue_shared/components/clone_dropdown_spec.js
index 5b8576ad761..eefd1838988 100644
--- a/spec/frontend/vue_shared/components/clone_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/clone_dropdown_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlFormInputGroup, GlDropdownSectionHeader } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import CloneDropdown from '~/vue_shared/components/clone_dropdown.vue';
describe('Clone Dropdown Button', () => {
diff --git a/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js b/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js
index c8fe6c3131c..d30f36ec63c 100644
--- a/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js
+++ b/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js
@@ -13,6 +13,7 @@ describe('ColorPicker', () => {
};
const setColor = '#000000';
+ const invalidText = 'Please enter a valid hex (#RRGGBB or #RGB) color value';
const label = () => wrapper.find(GlFormGroup).attributes('label');
const colorPreview = () => wrapper.find('[data-testid="color-preview"]');
const colorPicker = () => wrapper.find(GlFormInput);
@@ -28,8 +29,6 @@ describe('ColorPicker', () => {
'#428BCA': 'Moderate blue',
'#44AD8E': 'Lime green',
};
-
- createComponent(shallowMount);
});
afterEach(() => {
@@ -38,6 +37,8 @@ describe('ColorPicker', () => {
describe('label', () => {
it('hides the label if the label is not passed', () => {
+ createComponent(shallowMount);
+
expect(label()).toBe('');
});
@@ -55,43 +56,37 @@ describe('ColorPicker', () => {
expect(colorPreview().attributes('style')).toBe(undefined);
expect(colorPicker().attributes('value')).toBe(undefined);
expect(colorInput().props('value')).toBe('');
+ expect(colorPreview().attributes('class')).toContain('gl-inset-border-1-gray-400');
});
it('has a color set on initialization', () => {
- createComponent(shallowMount, { setColor });
+ createComponent(mount, { value: setColor });
- expect(wrapper.vm.$data.selectedColor).toBe(setColor);
+ expect(colorInput().props('value')).toBe(setColor);
});
it('emits input event from component when a color is selected', async () => {
createComponent();
await colorInput().setValue(setColor);
- expect(wrapper.emitted().input[0]).toEqual([setColor]);
+ expect(wrapper.emitted().input[0]).toStrictEqual([setColor]);
});
it('trims spaces from submitted colors', async () => {
createComponent();
await colorInput().setValue(` ${setColor} `);
- expect(wrapper.vm.$data.selectedColor).toBe(setColor);
+ expect(wrapper.emitted().input[0]).toStrictEqual([setColor]);
+ expect(colorPreview().attributes('class')).toContain('gl-inset-border-1-gray-400');
+ expect(colorInput().attributes('class')).not.toContain('is-invalid');
});
- it('shows invalid feedback when an invalid color is used', async () => {
- createComponent();
- await colorInput().setValue('abcd');
-
- expect(invalidFeedback().text()).toBe(
- 'Please enter a valid hex (#RRGGBB or #RGB) color value',
- );
- expect(wrapper.emitted().input).toBe(undefined);
- });
-
- it('shows an invalid feedback border on the preview when an invalid color is used', async () => {
- createComponent();
- await colorInput().setValue('abcd');
+ it('shows invalid feedback when the state is marked as invalid', async () => {
+ createComponent(mount, { invalidFeedback: invalidText, state: false });
+ expect(invalidFeedback().text()).toBe(invalidText);
expect(colorPreview().attributes('class')).toContain('gl-inset-border-1-red-500');
+ expect(colorInput().attributes('class')).toContain('is-invalid');
});
});
@@ -100,14 +95,14 @@ describe('ColorPicker', () => {
createComponent();
await colorInput().setValue(setColor);
- expect(wrapper.vm.$data.selectedColor).toBe(setColor);
+ expect(wrapper.emitted().input[0]).toStrictEqual([setColor]);
});
it('has color picker value entered', async () => {
createComponent();
await colorPicker().setValue(setColor);
- expect(wrapper.vm.$data.selectedColor).toBe(setColor);
+ expect(wrapper.emitted().input[0]).toStrictEqual([setColor]);
});
});
@@ -132,7 +127,7 @@ describe('ColorPicker', () => {
createComponent();
await presetColors().at(0).trigger('click');
- expect(wrapper.vm.$data.selectedColor).toBe(setColor);
+ expect(wrapper.emitted().input[0]).toStrictEqual([setColor]);
});
});
});
diff --git a/spec/frontend/vue_shared/components/commit_spec.js b/spec/frontend/vue_shared/components/commit_spec.js
index 6f3c97f7194..66ceebed489 100644
--- a/spec/frontend/vue_shared/components/commit_spec.js
+++ b/spec/frontend/vue_shared/components/commit_spec.js
@@ -1,5 +1,6 @@
-import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import CommitComponent from '~/vue_shared/components/commit.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
@@ -13,11 +14,14 @@ describe('Commit component', () => {
};
const findUserAvatar = () => wrapper.find(UserAvatarLink);
+ const findRefName = () => wrapper.findByTestId('ref-name');
const createComponent = (propsData) => {
- wrapper = shallowMount(CommitComponent, {
- propsData,
- });
+ wrapper = extendedWrapper(
+ shallowMount(CommitComponent, {
+ propsData,
+ }),
+ );
};
afterEach(() => {
@@ -223,4 +227,20 @@ describe('Commit component', () => {
expect(wrapper.find('.ref-name').exists()).toBe(false);
});
});
+
+ describe('When commitRef has a path property instead of ref_url property', () => {
+ it('should render path as href attribute', () => {
+ props = {
+ commitRef: {
+ name: 'master',
+ path: 'http://localhost/namespace2/gitlabhq/tree/master',
+ },
+ };
+
+ createComponent(props);
+
+ expect(findRefName().exists()).toBe(true);
+ expect(findRefName().attributes('href')).toBe(props.commitRef.path);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/content_viewer/viewers/image_viewer_spec.js b/spec/frontend/vue_shared/components/content_viewer/viewers/image_viewer_spec.js
index 31e843297fa..af3b63ad7e5 100644
--- a/spec/frontend/vue_shared/components/content_viewer/viewers/image_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/content_viewer/viewers/image_viewer_spec.js
@@ -33,4 +33,14 @@ describe('Image Viewer', () => {
},
);
});
+
+ describe('file path', () => {
+ it('should output a valid URL path for the image', () => {
+ wrapper = mount(ImageViewer, {
+ propsData: { path: '/url/hello#1.jpg' },
+ });
+
+ expect(wrapper.find('img').attributes('src')).toBe('/url/hello%231.jpg');
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js b/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js
index 22ee6acfed8..3ffb23dc7a0 100644
--- a/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js
@@ -1,6 +1,6 @@
-import $ from 'jquery';
-import MockAdapter from 'axios-mock-adapter';
import { mount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import $ from 'jquery';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import MarkdownViewer from '~/vue_shared/components/content_viewer/viewers/markdown_viewer.vue';
diff --git a/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js b/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
index b6bffbcc6f3..eacc41ccdad 100644
--- a/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
@@ -1,6 +1,6 @@
+import { mount } from '@vue/test-utils';
import Vue from 'vue';
import { compileToFunctions } from 'vue-template-compiler';
-import { mount } from '@vue/test-utils';
import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants';
import imageDiffViewer from '~/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue';
diff --git a/spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js b/spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js
index e91e6577aaf..9f433816b34 100644
--- a/spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js
+++ b/spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js
@@ -1,6 +1,5 @@
-import Vuex from 'vuex';
import { createLocalVue, shallowMount, mount } from '@vue/test-utils';
-import Renamed from '~/vue_shared/components/diff_viewer/viewers/renamed.vue';
+import Vuex from 'vuex';
import {
TRANSITION_LOAD_START,
TRANSITION_LOAD_ERROR,
@@ -10,6 +9,7 @@ import {
STATE_LOADING,
STATE_ERRORED,
} from '~/diffs/constants';
+import Renamed from '~/vue_shared/components/diff_viewer/viewers/renamed.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
diff --git a/spec/frontend/vue_shared/components/dismissible_alert_spec.js b/spec/frontend/vue_shared/components/dismissible_alert_spec.js
index 17905254292..cfa6d1064e5 100644
--- a/spec/frontend/vue_shared/components/dismissible_alert_spec.js
+++ b/spec/frontend/vue_shared/components/dismissible_alert_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlAlert } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import DismissibleAlert from '~/vue_shared/components/dismissible_alert.vue';
const TEST_HTML = 'Hello World! <strong>Foo</strong>';
diff --git a/spec/frontend/vue_shared/components/dismissible_container_spec.js b/spec/frontend/vue_shared/components/dismissible_container_spec.js
index 2c0e363fa0e..b8aeea38e77 100644
--- a/spec/frontend/vue_shared/components/dismissible_container_spec.js
+++ b/spec/frontend/vue_shared/components/dismissible_container_spec.js
@@ -1,5 +1,5 @@
-import MockAdapter from 'axios-mock-adapter';
import { shallowMount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import dismissibleContainer from '~/vue_shared/components/dismissible_container.vue';
diff --git a/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js b/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js
index 4c4baf23120..175d79dd1c2 100644
--- a/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js
+++ b/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js
@@ -1,5 +1,5 @@
-import { mount, shallowMount } from '@vue/test-utils';
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import Component from '~/vue_shared/components/dismissible_feedback_alert.vue';
diff --git a/spec/frontend/vue_shared/components/editor_lite_spec.js b/spec/frontend/vue_shared/components/editor_lite_spec.js
index 70fdd8e24a5..badd5aed0e3 100644
--- a/spec/frontend/vue_shared/components/editor_lite_spec.js
+++ b/spec/frontend/vue_shared/components/editor_lite_spec.js
@@ -1,7 +1,8 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import EditorLite from '~/vue_shared/components/editor_lite.vue';
+import { EDITOR_READY_EVENT } from '~/editor/constants';
import Editor from '~/editor/editor_lite';
+import EditorLite from '~/vue_shared/components/editor_lite.vue';
jest.mock('~/editor/editor_lite');
@@ -110,13 +111,13 @@ describe('Editor Lite component', () => {
expect(wrapper.emitted().input).toEqual([[value]]);
});
- it('emits editor-ready event when the Editor Lite is ready', async () => {
+ it('emits EDITOR_READY_EVENT event when the Editor Lite is ready', async () => {
const el = wrapper.find({ ref: 'editor' }).element;
- expect(wrapper.emitted()['editor-ready']).toBeUndefined();
+ expect(wrapper.emitted()[EDITOR_READY_EVENT]).toBeUndefined();
- await el.dispatchEvent(new Event('editor-ready'));
+ await el.dispatchEvent(new Event(EDITOR_READY_EVENT));
- expect(wrapper.emitted()['editor-ready']).toBeDefined();
+ expect(wrapper.emitted()[EDITOR_READY_EVENT]).toBeDefined();
});
it('component API `getEditor()` returns the editor instance', () => {
diff --git a/spec/frontend/vue_shared/components/expand_button_spec.js b/spec/frontend/vue_shared/components/expand_button_spec.js
index 724405a109f..7874658cc0f 100644
--- a/spec/frontend/vue_shared/components/expand_button_spec.js
+++ b/spec/frontend/vue_shared/components/expand_button_spec.js
@@ -1,5 +1,5 @@
-import Vue from 'vue';
import { mount } from '@vue/test-utils';
+import Vue from 'vue';
import ExpandButton from '~/vue_shared/components/expand_button.vue';
const text = {
diff --git a/spec/frontend/vue_shared/components/file_finder/index_spec.js b/spec/frontend/vue_shared/components/file_finder/index_spec.js
index 238a5440664..d757b7fac72 100644
--- a/spec/frontend/vue_shared/components/file_finder/index_spec.js
+++ b/spec/frontend/vue_shared/components/file_finder/index_spec.js
@@ -1,9 +1,9 @@
-import Vue from 'vue';
import Mousetrap from 'mousetrap';
-import { file } from 'jest/ide/helpers';
+import Vue from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import FindFileComponent from '~/vue_shared/components/file_finder/index.vue';
+import { file } from 'jest/ide/helpers';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
+import FindFileComponent from '~/vue_shared/components/file_finder/index.vue';
describe('File finder item spec', () => {
const Component = Vue.extend(FindFileComponent);
diff --git a/spec/frontend/vue_shared/components/file_finder/item_spec.js b/spec/frontend/vue_shared/components/file_finder/item_spec.js
index c60e6335389..1a4a97efb95 100644
--- a/spec/frontend/vue_shared/components/file_finder/item_spec.js
+++ b/spec/frontend/vue_shared/components/file_finder/item_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import { file } from 'jest/ide/helpers';
import createComponent from 'helpers/vue_mount_component_helper';
+import { file } from 'jest/ide/helpers';
import ItemComponent from '~/vue_shared/components/file_finder/item.vue';
describe('File finder item spec', () => {
diff --git a/spec/frontend/vue_shared/components/file_icon_spec.js b/spec/frontend/vue_shared/components/file_icon_spec.js
index e55449dc684..c10663f6c14 100644
--- a/spec/frontend/vue_shared/components/file_icon_spec.js
+++ b/spec/frontend/vue_shared/components/file_icon_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import { FILE_SYMLINK_MODE } from '~/vue_shared/constants';
diff --git a/spec/frontend/vue_shared/components/file_row_spec.js b/spec/frontend/vue_shared/components/file_row_spec.js
index bd6a18bf704..62fb29c455c 100644
--- a/spec/frontend/vue_shared/components/file_row_spec.js
+++ b/spec/frontend/vue_shared/components/file_row_spec.js
@@ -1,10 +1,10 @@
-import { file } from 'jest/ide/helpers';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { file } from 'jest/ide/helpers';
+import { escapeFileUrl } from '~/lib/utils/url_utility';
+import FileIcon from '~/vue_shared/components/file_icon.vue';
import FileRow from '~/vue_shared/components/file_row.vue';
import FileHeader from '~/vue_shared/components/file_row_header.vue';
-import FileIcon from '~/vue_shared/components/file_icon.vue';
-import { escapeFileUrl } from '~/lib/utils/url_utility';
describe('File row component', () => {
let wrapper;
diff --git a/spec/frontend/vue_shared/components/file_tree_spec.js b/spec/frontend/vue_shared/components/file_tree_spec.js
index 7a4982fd29b..39a7c7a2b3a 100644
--- a/spec/frontend/vue_shared/components/file_tree_spec.js
+++ b/spec/frontend/vue_shared/components/file_tree_spec.js
@@ -1,5 +1,5 @@
-import { pick } from 'lodash';
import { shallowMount } from '@vue/test-utils';
+import { pick } from 'lodash';
import FileTree from '~/vue_shared/components/file_tree.vue';
const MockFileRow = {
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
index b58ce0083c0..9fa9d35e3e2 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
@@ -1,4 +1,3 @@
-import { shallowMount, mount } from '@vue/test-utils';
import {
GlFilteredSearch,
GlButtonGroup,
@@ -7,13 +6,13 @@ import {
GlDropdownItem,
GlFormCheckbox,
} from '@gitlab/ui';
+import { shallowMount, mount } from '@vue/test-utils';
+import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
+import RecentSearchesStore from '~/filtered_search/stores/recent_searches_store';
+import { SortDirection } from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchBarRoot from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import { uniqueTokens } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
-import { SortDirection } from '~/vue_shared/components/filtered_search_bar/constants';
-
-import RecentSearchesStore from '~/filtered_search/stores/recent_searches_store';
-import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
import {
mockAvailableTokens,
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js
index 1dd5f08e76a..05bad572472 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js
@@ -2,12 +2,12 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { mockBranches } from 'jest/vue_shared/components/filtered_search_bar/mock_data';
+import Api from '~/api';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+import httpStatusCodes from '~/lib/utils/http_status';
import * as actions from '~/vue_shared/components/filtered_search_bar/store/modules/filters/actions';
import * as types from '~/vue_shared/components/filtered_search_bar/store/modules/filters/mutation_types';
import initialState from '~/vue_shared/components/filtered_search_bar/store/modules/filters/state';
-import httpStatusCodes from '~/lib/utils/http_status';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
-import Api from '~/api';
import { filterMilestones, filterUsers, filterLabels } from './mock_data';
const milestonesEndpoint = 'fake_milestones_endpoint';
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/mutations_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/mutations_spec.js
index 263a4ee178f..66c6267027b 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/mutations_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/mutations_spec.js
@@ -1,9 +1,9 @@
import { get } from 'lodash';
import { mockBranches } from 'jest/vue_shared/components/filtered_search_bar/mock_data';
-import initialState from '~/vue_shared/components/filtered_search_bar/store/modules/filters/state';
-import mutations from '~/vue_shared/components/filtered_search_bar/store/modules/filters/mutations';
-import * as types from '~/vue_shared/components/filtered_search_bar/store/modules/filters/mutation_types';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import * as types from '~/vue_shared/components/filtered_search_bar/store/modules/filters/mutation_types';
+import mutations from '~/vue_shared/components/filtered_search_bar/store/modules/filters/mutations';
+import initialState from '~/vue_shared/components/filtered_search_bar/store/modules/filters/state';
import { filterMilestones, filterUsers, filterLabels } from './mock_data';
let state = null;
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js
index 3997d6a99a6..765e576914c 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js
@@ -1,15 +1,15 @@
-import { mount } from '@vue/test-utils';
import {
GlFilteredSearchToken,
GlFilteredSearchTokenSegment,
GlFilteredSearchSuggestion,
GlDropdownDivider,
} from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
import {
DEFAULT_LABEL_NONE,
DEFAULT_LABEL_ANY,
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
index 35f487330be..a20bc4986fc 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
@@ -1,15 +1,15 @@
-import { mount } from '@vue/test-utils';
import {
GlFilteredSearchToken,
GlFilteredSearchSuggestion,
GlFilteredSearchTokenSegment,
GlDropdownDivider,
} from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
-import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
import {
DEFAULT_LABEL_NONE,
DEFAULT_LABEL_ANY,
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
index dda0ad39bbc..7676ce10ce0 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
@@ -1,19 +1,19 @@
-import { mount } from '@vue/test-utils';
import {
GlFilteredSearchToken,
GlFilteredSearchSuggestion,
GlFilteredSearchTokenSegment,
GlDropdownDivider,
} from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import {
mockRegularLabel,
mockLabels,
} from 'jest/vue_shared/components/sidebar/labels_select_vue/mock_data';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
import {
DEFAULT_LABELS,
DEFAULT_LABEL_NONE,
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
index 164561f6244..9f550ac9afc 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
@@ -1,15 +1,15 @@
-import { mount } from '@vue/test-utils';
import {
GlFilteredSearchToken,
GlFilteredSearchSuggestion,
GlFilteredSearchTokenSegment,
GlDropdownDivider,
} from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import createFlash from '~/flash';
import { DEFAULT_MILESTONES } from '~/vue_shared/components/filtered_search_bar/constants';
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
diff --git a/spec/frontend/vue_shared/components/gfm_autocomplete/__snapshots__/utils_spec.js.snap b/spec/frontend/vue_shared/components/gfm_autocomplete/__snapshots__/utils_spec.js.snap
index d8e6e37bb89..370b6eb01bc 100644
--- a/spec/frontend/vue_shared/components/gfm_autocomplete/__snapshots__/utils_spec.js.snap
+++ b/spec/frontend/vue_shared/components/gfm_autocomplete/__snapshots__/utils_spec.js.snap
@@ -1,12 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`gfm_autocomplete/utils emojis config shows the emoji name and icon in the menu item 1`] = `
-"raised_hands
- <gl-emoji
-
- data-name=\\"raised_hands\\"></gl-emoji>
- "
-`;
+exports[`gfm_autocomplete/utils emojis config shows the emoji name and icon in the menu item 1`] = `"raised_hands <gl-emoji data-name=\\"raised_hands\\"></gl-emoji>"`;
exports[`gfm_autocomplete/utils issues config shows the iid and title in the menu item within a project context 1`] = `"<small>123456</small> Project context issue title &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;"`;
@@ -21,10 +15,10 @@ exports[`gfm_autocomplete/utils labels config shows the title in the menu item 1
exports[`gfm_autocomplete/utils members config shows an avatar character, name, parent name, and count in the menu item for a group 1`] = `
"
<div class=\\"gl-display-flex gl-align-items-center\\">
- <div class=\\"gl-avatar gl-avatar-s24 gl-flex-shrink-0 gl-rounded-small
+ <div class=\\"gl-avatar gl-avatar-s32 gl-flex-shrink-0 gl-rounded-small
gl-display-flex gl-align-items-center gl-justify-content-center\\" aria-hidden=\\"true\\">
G</div>
- <div class=\\"gl-font-sm gl-line-height-normal gl-ml-3\\">
+ <div class=\\"gl-line-height-normal gl-ml-4\\">
<div>1-1s &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt; (2)</div>
<div class=\\"gl-text-gray-700\\">GitLab Support Team</div>
</div>
@@ -36,8 +30,8 @@ exports[`gfm_autocomplete/utils members config shows an avatar character, name,
exports[`gfm_autocomplete/utils members config shows the avatar, name and username in the menu item for a user 1`] = `
"
<div class=\\"gl-display-flex gl-align-items-center\\">
- <img class=\\"gl-avatar gl-avatar-s24 gl-flex-shrink-0 gl-avatar-circle\\" src=\\"/uploads/-/system/user/avatar/123456/avatar.png\\" alt=\\"\\" />
- <div class=\\"gl-font-sm gl-line-height-normal gl-ml-3\\">
+ <img class=\\"gl-avatar gl-avatar-s32 gl-flex-shrink-0 gl-avatar-circle\\" src=\\"/uploads/-/system/user/avatar/123456/avatar.png\\" alt=\\"\\" />
+ <div class=\\"gl-line-height-normal gl-ml-4\\">
<div>My Name &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;</div>
<div class=\\"gl-text-gray-700\\">@myusername</div>
</div>
diff --git a/spec/frontend/vue_shared/components/gl_countdown_spec.js b/spec/frontend/vue_shared/components/gl_countdown_spec.js
index fcc5c0cd310..82d18c7fd3f 100644
--- a/spec/frontend/vue_shared/components/gl_countdown_spec.js
+++ b/spec/frontend/vue_shared/components/gl_countdown_spec.js
@@ -1,5 +1,5 @@
-import mountComponent from 'helpers/vue_mount_component_helper';
import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
describe('GlCountdown', () => {
diff --git a/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js b/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js
index 6802499ed52..390a70792f3 100644
--- a/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js
+++ b/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js
@@ -1,6 +1,7 @@
+import { GlModal } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import { GlModal } from '@gitlab/ui';
+import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
import GlModalVuex from '~/vue_shared/components/gl_modal_vuex.vue';
import createState from '~/vuex_shared/modules/modal/state';
@@ -129,7 +130,7 @@ describe('GlModalVuex', () => {
wrapper.vm
.$nextTick()
.then(() => {
- expect(rootEmit).toHaveBeenCalledWith('bv::show::modal', TEST_MODAL_ID);
+ expect(rootEmit).toHaveBeenCalledWith(BV_SHOW_MODAL, TEST_MODAL_ID);
})
.then(done)
.catch(done.fail);
@@ -146,7 +147,7 @@ describe('GlModalVuex', () => {
wrapper.vm
.$nextTick()
.then(() => {
- expect(rootEmit).toHaveBeenCalledWith('bv::hide::modal', TEST_MODAL_ID);
+ expect(rootEmit).toHaveBeenCalledWith(BV_HIDE_MODAL, TEST_MODAL_ID);
})
.then(done)
.catch(done.fail);
diff --git a/spec/frontend/vue_shared/components/gl_toggle_vuex_spec.js b/spec/frontend/vue_shared/components/gl_toggle_vuex_spec.js
index 30afb044bbf..ac670b622b1 100644
--- a/spec/frontend/vue_shared/components/gl_toggle_vuex_spec.js
+++ b/spec/frontend/vue_shared/components/gl_toggle_vuex_spec.js
@@ -1,6 +1,6 @@
-import Vuex from 'vuex';
import { GlToggle } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
import GlToggleVuex from '~/vue_shared/components/gl_toggle_vuex.vue';
const localVue = createLocalVue();
diff --git a/spec/frontend/vue_shared/components/header_ci_component_spec.js b/spec/frontend/vue_shared/components/header_ci_component_spec.js
index 5233a64ce5e..b54d120b55b 100644
--- a/spec/frontend/vue_shared/components/header_ci_component_spec.js
+++ b/spec/frontend/vue_shared/components/header_ci_component_spec.js
@@ -1,93 +1,103 @@
-import Vue from 'vue';
-import mountComponent, { mountComponentWithSlots } from 'helpers/vue_mount_component_helper';
-import headerCi from '~/vue_shared/components/header_ci_component.vue';
+import { GlButton, GlLink } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import CiIconBadge from '~/vue_shared/components/ci_badge_link.vue';
+import HeaderCi from '~/vue_shared/components/header_ci_component.vue';
+import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
describe('Header CI Component', () => {
- let HeaderCi;
- let vm;
- let props;
-
- beforeEach(() => {
- HeaderCi = Vue.extend(headerCi);
- props = {
- status: {
- group: 'failed',
- icon: 'status_failed',
- label: 'failed',
- text: 'failed',
- details_path: 'path',
- },
- itemName: 'job',
- itemId: 123,
- time: '2017-05-08T14:57:39.781Z',
- user: {
- web_url: 'path',
- name: 'Foo',
- username: 'foobar',
- email: 'foo@bar.com',
- avatar_url: 'link',
- },
- hasSidebarButton: true,
- };
- });
+ let wrapper;
+
+ const defaultProps = {
+ status: {
+ group: 'failed',
+ icon: 'status_failed',
+ label: 'failed',
+ text: 'failed',
+ details_path: 'path',
+ },
+ itemName: 'job',
+ itemId: 123,
+ time: '2017-05-08T14:57:39.781Z',
+ user: {
+ web_url: 'path',
+ name: 'Foo',
+ username: 'foobar',
+ email: 'foo@bar.com',
+ avatar_url: 'link',
+ },
+ hasSidebarButton: true,
+ };
+
+ const findIconBadge = () => wrapper.findComponent(CiIconBadge);
+ const findTimeAgo = () => wrapper.findComponent(TimeagoTooltip);
+ const findUserLink = () => wrapper.findComponent(GlLink);
+ const findSidebarToggleBtn = () => wrapper.findComponent(GlButton);
+ const findActionButtons = () => wrapper.findByTestId('ci-header-action-buttons');
+ const findHeaderItemText = () => wrapper.findByTestId('ci-header-item-text');
+
+ const createComponent = (props, slots) => {
+ wrapper = extendedWrapper(
+ shallowMount(HeaderCi, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ ...slots,
+ }),
+ );
+ };
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
+ wrapper = null;
});
- const findActionButtons = () => vm.$el.querySelector('[data-testid="headerButtons"]');
-
describe('render', () => {
beforeEach(() => {
- vm = mountComponent(HeaderCi, props);
+ createComponent();
});
it('should render status badge', () => {
- expect(vm.$el.querySelector('.ci-failed')).toBeDefined();
- expect(vm.$el.querySelector('.ci-status-icon-failed svg')).toBeDefined();
- expect(vm.$el.querySelector('.ci-failed').getAttribute('href')).toEqual(
- props.status.details_path,
- );
+ expect(findIconBadge().exists()).toBe(true);
});
it('should render item name and id', () => {
- expect(vm.$el.querySelector('strong').textContent.trim()).toEqual('job #123');
+ expect(findHeaderItemText().text()).toBe('job #123');
});
it('should render timeago date', () => {
- expect(vm.$el.querySelector('time')).toBeDefined();
+ expect(findTimeAgo().exists()).toBe(true);
});
it('should render user icon and name', () => {
- expect(vm.$el.querySelector('.js-user-link').innerText.trim()).toContain(props.user.name);
+ expect(findUserLink().text()).toContain(defaultProps.user.name);
});
it('should render sidebar toggle button', () => {
- expect(vm.$el.querySelector('.js-sidebar-build-toggle')).not.toBeNull();
+ expect(findSidebarToggleBtn().exists()).toBe(true);
});
- it('should not render header action buttons when empty', () => {
- expect(findActionButtons()).toBeNull();
+ it('should not render header action buttons when slot is empty', () => {
+ expect(findActionButtons().exists()).toBe(false);
});
});
describe('slot', () => {
it('should render header action buttons', () => {
- vm = mountComponentWithSlots(HeaderCi, { props, slots: { default: 'Test Actions' } });
-
- const buttons = findActionButtons();
+ createComponent({}, { slots: { default: 'Test Actions' } });
- expect(buttons).not.toBeNull();
- expect(buttons.textContent).toEqual('Test Actions');
+ expect(findActionButtons().exists()).toBe(true);
+ expect(findActionButtons().text()).toBe('Test Actions');
});
});
describe('shouldRenderTriggeredLabel', () => {
- it('should rendered created keyword when the shouldRenderTriggeredLabel is false', () => {
- vm = mountComponent(HeaderCi, { ...props, shouldRenderTriggeredLabel: false });
+ it('should render created keyword when the shouldRenderTriggeredLabel is false', () => {
+ createComponent({ shouldRenderTriggeredLabel: false });
- expect(vm.$el.textContent).toContain('created');
- expect(vm.$el.textContent).not.toContain('triggered');
+ expect(wrapper.text()).toContain('created');
+ expect(wrapper.text()).not.toContain('triggered');
});
});
});
diff --git a/spec/frontend/vue_shared/components/help_popover_spec.js b/spec/frontend/vue_shared/components/help_popover_spec.js
new file mode 100644
index 00000000000..baf80a8a04e
--- /dev/null
+++ b/spec/frontend/vue_shared/components/help_popover_spec.js
@@ -0,0 +1,65 @@
+import { GlButton, GlPopover } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import HelpPopover from '~/vue_shared/components/help_popover.vue';
+
+describe('HelpPopover', () => {
+ let wrapper;
+ const title = 'popover <strong>title</strong>';
+ const content = 'popover <b>content</b>';
+
+ const findQuestionButton = () => wrapper.find(GlButton);
+ const findPopover = () => wrapper.find(GlPopover);
+ const buildWrapper = (options = {}) => {
+ wrapper = mount(HelpPopover, {
+ propsData: {
+ options: {
+ title,
+ content,
+ ...options,
+ },
+ },
+ });
+ };
+
+ beforeEach(() => {
+ buildWrapper();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('renders a link button with an icon question', () => {
+ expect(findQuestionButton().props()).toMatchObject({
+ icon: 'question',
+ variant: 'link',
+ });
+ expect(findQuestionButton().attributes().tabindex).toBe('0');
+ });
+
+ it('renders popover that uses the question button as target', () => {
+ expect(findPopover().props().target()).toBe(findQuestionButton().vm.$el);
+ });
+
+ it('triggers popover on hover and focus', () => {
+ expect(findPopover().props().triggers).toBe('hover focus');
+ });
+
+ it('allows rendering title with HTML tags', () => {
+ expect(findPopover().find('strong').exists()).toBe(true);
+ });
+
+ it('allows rendering content with HTML tags', () => {
+ expect(findPopover().find('b').exists()).toBe(true);
+ });
+
+ it('binds other popover options to the popover instance', () => {
+ const placement = 'bottom';
+
+ wrapper.destroy();
+ buildWrapper({ placement });
+
+ expect(findPopover().props().placement).toBe(placement);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/integration_help_text_spec.js b/spec/frontend/vue_shared/components/integration_help_text_spec.js
index 4269d36d0e2..c0e8b719007 100644
--- a/spec/frontend/vue_shared/components/integration_help_text_spec.js
+++ b/spec/frontend/vue_shared/components/integration_help_text_spec.js
@@ -1,6 +1,6 @@
+import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
import IntegrationHelpText from '~/vue_shared/components/integrations_help_text.vue';
describe('IntegrationHelpText component', () => {
diff --git a/spec/frontend/vue_shared/components/issuable/issuable_header_warnings_spec.js b/spec/frontend/vue_shared/components/issuable/issuable_header_warnings_spec.js
index a03a3915e1b..573501233b9 100644
--- a/spec/frontend/vue_shared/components/issuable/issuable_header_warnings_spec.js
+++ b/spec/frontend/vue_shared/components/issuable/issuable_header_warnings_spec.js
@@ -1,8 +1,8 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import IssuableHeaderWarnings from '~/vue_shared/components/issuable/issuable_header_warnings.vue';
-import createIssueStore from '~/notes/stores';
import { createStore as createMrStore } from '~/mr_notes/stores';
+import createIssueStore from '~/notes/stores';
+import IssuableHeaderWarnings from '~/vue_shared/components/issuable/issuable_header_warnings.vue';
const ISSUABLE_TYPE_ISSUE = 'issue';
const ISSUABLE_TYPE_MR = 'merge request';
diff --git a/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js b/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js
index 5f614bfc751..5c29c267c99 100644
--- a/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js
+++ b/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import { mockAssigneesList } from 'jest/boards/mock_data';
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
const TEST_CSS_CLASSES = 'test-classes';
const TEST_MAX_VISIBLE = 4;
diff --git a/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js b/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js
index ffcb891c4fc..9a121050225 100644
--- a/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js
+++ b/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js
@@ -1,8 +1,8 @@
-import Vue from 'vue';
+import { GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import { mockMilestone } from 'jest/boards/mock_data';
-import { GlIcon } from '@gitlab/ui';
import IssueMilestone from '~/vue_shared/components/issue/issue_milestone.vue';
const createComponent = (milestone = mockMilestone) => {
diff --git a/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js b/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js
index 3dc34583118..f34a2db0851 100644
--- a/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js
+++ b/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js
@@ -1,8 +1,8 @@
import { mount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
+import IssueDueDate from '~/boards/components/issue_due_date.vue';
import { formatDate } from '~/lib/utils/datetime_utility';
import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
-import IssueDueDate from '~/boards/components/issue_due_date.vue';
import { defaultAssignees, defaultMilestone } from './related_issuable_mock_data';
describe('RelatedIssuableItem', () => {
diff --git a/spec/frontend/vue_shared/components/markdown/apply_suggestion_spec.js b/spec/frontend/vue_shared/components/markdown/apply_suggestion_spec.js
index b9f0d88548d..c56628fcbcd 100644
--- a/spec/frontend/vue_shared/components/markdown/apply_suggestion_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/apply_suggestion_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlDropdown, GlFormTextarea, GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import ApplySuggestionComponent from '~/vue_shared/components/markdown/apply_suggestion.vue';
describe('Apply Suggestion component', () => {
diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js
index a2ce6f40193..442032840e1 100644
--- a/spec/frontend/vue_shared/components/markdown/field_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/field_spec.js
@@ -1,9 +1,9 @@
import { mount } from '@vue/test-utils';
-import { TEST_HOST, FIXTURES_PATH } from 'spec/test_constants';
import AxiosMockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
-import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import { TEST_HOST, FIXTURES_PATH } from 'spec/test_constants';
import axios from '~/lib/utils/axios_utils';
+import MarkdownField from '~/vue_shared/components/markdown/field.vue';
const markdownPreviewPath = `${TEST_HOST}/preview`;
const markdownDocsPath = `${TEST_HOST}/docs`;
diff --git a/spec/frontend/vue_shared/components/markdown/field_view_spec.js b/spec/frontend/vue_shared/components/markdown/field_view_spec.js
index 80cf1f655c6..be1d840dd29 100644
--- a/spec/frontend/vue_shared/components/markdown/field_view_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/field_view_spec.js
@@ -1,5 +1,5 @@
-import $ from 'jquery';
import { shallowMount } from '@vue/test-utils';
+import $ from 'jquery';
import MarkdownFieldView from '~/vue_shared/components/markdown/field_view.vue';
diff --git a/spec/frontend/vue_shared/components/memory_graph_spec.js b/spec/frontend/vue_shared/components/memory_graph_spec.js
index 9a5ee544d8f..53b96bd1b98 100644
--- a/spec/frontend/vue_shared/components/memory_graph_spec.js
+++ b/spec/frontend/vue_shared/components/memory_graph_spec.js
@@ -1,6 +1,6 @@
-import Vue from 'vue';
-import { shallowMount } from '@vue/test-utils';
import { GlSparklineChart } from '@gitlab/ui/dist/charts';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import MemoryGraph from '~/vue_shared/components/memory_graph.vue';
describe('MemoryGraph', () => {
diff --git a/spec/frontend/vue_shared/components/modal_copy_button_spec.js b/spec/frontend/vue_shared/components/modal_copy_button_spec.js
index ca9f8ff54d4..adb72c3ef85 100644
--- a/spec/frontend/vue_shared/components/modal_copy_button_spec.js
+++ b/spec/frontend/vue_shared/components/modal_copy_button_spec.js
@@ -1,4 +1,5 @@
import { shallowMount, createWrapper } from '@vue/test-utils';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
describe('modal copy button', () => {
@@ -31,7 +32,7 @@ describe('modal copy button', () => {
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted().success).not.toBeEmpty();
expect(document.execCommand).toHaveBeenCalledWith('copy');
- expect(root.emitted('bv::hide::tooltip')).toEqual([['test-id']]);
+ expect(root.emitted(BV_HIDE_TOOLTIP)).toEqual([['test-id']]);
});
});
it("should propagate the clipboard error event if execCommand doesn't work", () => {
diff --git a/spec/frontend/vue_shared/components/multiselect_dropdown_spec.js b/spec/frontend/vue_shared/components/multiselect_dropdown_spec.js
index 233c488b60b..99671f1ffb7 100644
--- a/spec/frontend/vue_shared/components/multiselect_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/multiselect_dropdown_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { getByText } from '@testing-library/dom';
+import { shallowMount } from '@vue/test-utils';
import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dropdown.vue';
describe('MultiSelectDropdown Component', () => {
diff --git a/spec/frontend/vue_shared/components/navigation_tabs_spec.js b/spec/frontend/vue_shared/components/navigation_tabs_spec.js
index b1119bfb150..30a89fed12f 100644
--- a/spec/frontend/vue_shared/components/navigation_tabs_spec.js
+++ b/spec/frontend/vue_shared/components/navigation_tabs_spec.js
@@ -1,5 +1,5 @@
-import { mount } from '@vue/test-utils';
import { GlTab } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
describe('navigation tabs component', () => {
diff --git a/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js b/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js
index cc9f05beb06..835759b1f20 100644
--- a/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js
+++ b/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue';
describe('Issue Warning Component', () => {
diff --git a/spec/frontend/vue_shared/components/notes/system_note_spec.js b/spec/frontend/vue_shared/components/notes/system_note_spec.js
index d5eac7c2aa3..48dacc50923 100644
--- a/spec/frontend/vue_shared/components/notes/system_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/system_note_spec.js
@@ -1,7 +1,7 @@
import { mount } from '@vue/test-utils';
-import IssueSystemNote from '~/vue_shared/components/notes/system_note.vue';
-import createStore from '~/notes/stores';
import initMRPopovers from '~/mr_popover/index';
+import createStore from '~/notes/stores';
+import IssueSystemNote from '~/vue_shared/components/notes/system_note.vue';
jest.mock('~/mr_popover/index', () => jest.fn());
diff --git a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
index 491f783622a..74e9cbcbb53 100644
--- a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
+++ b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
@@ -1,9 +1,9 @@
-import { mount } from '@vue/test-utils';
import { GlAlert, GlBadge, GlPagination, GlTabs, GlTab } from '@gitlab/ui';
-import PageWrapper from '~/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue';
+import { mount } from '@vue/test-utils';
+import Tracking from '~/tracking';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
-import Tracking from '~/tracking';
+import PageWrapper from '~/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue';
import mockItems from './mocks/items.json';
import mockFilters from './mocks/items_filters.json';
diff --git a/spec/frontend/vue_shared/components/pagination_links_spec.js b/spec/frontend/vue_shared/components/pagination_links_spec.js
index ad82aee0098..83f1e2844f9 100644
--- a/spec/frontend/vue_shared/components/pagination_links_spec.js
+++ b/spec/frontend/vue_shared/components/pagination_links_spec.js
@@ -1,6 +1,5 @@
-import { mount } from '@vue/test-utils';
import { GlPagination } from '@gitlab/ui';
-import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
+import { mount } from '@vue/test-utils';
import {
PREV,
NEXT,
@@ -9,6 +8,7 @@ import {
LABEL_NEXT_PAGE,
LABEL_LAST_PAGE,
} from '~/vue_shared/components/pagination/constants';
+import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
describe('Pagination links component', () => {
const pageInfo = {
diff --git a/spec/frontend/vue_shared/components/pikaday_spec.js b/spec/frontend/vue_shared/components/pikaday_spec.js
index 1c6876c282c..fed4ce5e696 100644
--- a/spec/frontend/vue_shared/components/pikaday_spec.js
+++ b/spec/frontend/vue_shared/components/pikaday_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlDatepicker } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import datePicker from '~/vue_shared/components/pikaday.vue';
describe('datePicker', () => {
diff --git a/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js b/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js
index 016622fd0bb..06b00a8e196 100644
--- a/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js
+++ b/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js
@@ -1,8 +1,7 @@
-import Vue from 'vue';
-import { head } from 'lodash';
-
import { GlSearchBoxByType, GlInfiniteScroll } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
+import { head } from 'lodash';
+import Vue from 'vue';
import { trimText } from 'helpers/text_helper';
import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue';
import ProjectSelector from '~/vue_shared/components/project_selector/project_selector.vue';
diff --git a/spec/frontend/vue_shared/components/registry/code_instruction_spec.js b/spec/frontend/vue_shared/components/registry/code_instruction_spec.js
index 84c738764a3..4ec608aaf07 100644
--- a/spec/frontend/vue_shared/components/registry/code_instruction_spec.js
+++ b/spec/frontend/vue_shared/components/registry/code_instruction_spec.js
@@ -1,7 +1,7 @@
import { mount } from '@vue/test-utils';
import Tracking from '~/tracking';
-import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
describe('Package code instruction', () => {
let wrapper;
diff --git a/spec/frontend/vue_shared/components/registry/details_row_spec.js b/spec/frontend/vue_shared/components/registry/details_row_spec.js
index 09dacfae363..3134e0d3e21 100644
--- a/spec/frontend/vue_shared/components/registry/details_row_spec.js
+++ b/spec/frontend/vue_shared/components/registry/details_row_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import component from '~/vue_shared/components/registry/details_row.vue';
describe('DetailsRow', () => {
diff --git a/spec/frontend/vue_shared/components/registry/history_item_spec.js b/spec/frontend/vue_shared/components/registry/history_item_spec.js
index d51ddda2e3e..f146f87342f 100644
--- a/spec/frontend/vue_shared/components/registry/history_item_spec.js
+++ b/spec/frontend/vue_shared/components/registry/history_item_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import component from '~/vue_shared/components/registry/history_item.vue';
diff --git a/spec/frontend/vue_shared/components/registry/metadata_item_spec.js b/spec/frontend/vue_shared/components/registry/metadata_item_spec.js
index 3d3cfbe13e3..1ccf3ddc5a5 100644
--- a/spec/frontend/vue_shared/components/registry/metadata_item_spec.js
+++ b/spec/frontend/vue_shared/components/registry/metadata_item_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlIcon, GlLink } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import component from '~/vue_shared/components/registry/metadata_item.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
diff --git a/spec/frontend/vue_shared/components/registry/registry_search_spec.js b/spec/frontend/vue_shared/components/registry/registry_search_spec.js
new file mode 100644
index 00000000000..28bdb275756
--- /dev/null
+++ b/spec/frontend/vue_shared/components/registry/registry_search_spec.js
@@ -0,0 +1,105 @@
+import { GlSorting, GlSortingItem, GlFilteredSearch } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import component from '~/vue_shared/components/registry/registry_search.vue';
+
+describe('Registry Search', () => {
+ let wrapper;
+
+ const findPackageListSorting = () => wrapper.find(GlSorting);
+ const findSortingItems = () => wrapper.findAll(GlSortingItem);
+ const findFilteredSearch = () => wrapper.find(GlFilteredSearch);
+
+ const defaultProps = {
+ filter: [],
+ sorting: { sort: 'asc', orderBy: 'name' },
+ tokens: ['foo'],
+ sortableFields: [{ label: 'name', orderBy: 'name' }, { label: 'baz' }],
+ };
+
+ const mountComponent = (propsData = defaultProps) => {
+ wrapper = shallowMount(component, {
+ propsData,
+ stubs: {
+ GlSortingItem,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('searching', () => {
+ it('has a filtered-search component', () => {
+ mountComponent();
+
+ expect(findFilteredSearch().exists()).toBe(true);
+ });
+
+ it('binds the correct props to filtered-search', () => {
+ mountComponent();
+
+ expect(findFilteredSearch().props()).toMatchObject({
+ value: [],
+ placeholder: 'Filter results',
+ availableTokens: wrapper.vm.tokens,
+ });
+ });
+
+ it('emits filter:changed when value changes', () => {
+ mountComponent();
+
+ findFilteredSearch().vm.$emit('input', 'foo');
+
+ expect(wrapper.emitted('filter:changed')).toEqual([['foo']]);
+ });
+
+ it('emits filter:submit on submit event', () => {
+ mountComponent();
+
+ findFilteredSearch().vm.$emit('submit');
+ expect(wrapper.emitted('filter:submit')).toEqual([[]]);
+ });
+
+ it('emits filter:changed and filter:submit on clear event', () => {
+ mountComponent();
+
+ findFilteredSearch().vm.$emit('clear');
+
+ expect(wrapper.emitted('filter:changed')).toEqual([[[]]]);
+ expect(wrapper.emitted('filter:submit')).toEqual([[]]);
+ });
+
+ it('binds tokens prop', () => {
+ mountComponent();
+
+ expect(findFilteredSearch().props('availableTokens')).toEqual(defaultProps.tokens);
+ });
+ });
+
+ describe('sorting', () => {
+ it('has all the sortable items', () => {
+ mountComponent();
+
+ expect(findSortingItems()).toHaveLength(defaultProps.sortableFields.length);
+ });
+
+ it('on sort change emits sorting:changed event', () => {
+ mountComponent();
+
+ findPackageListSorting().vm.$emit('sortDirectionChange');
+ expect(wrapper.emitted('sorting:changed')).toEqual([[{ sort: 'desc' }]]);
+ });
+
+ it('on sort item click emits sorting:changed event ', () => {
+ mountComponent();
+
+ findSortingItems().at(0).vm.$emit('click');
+
+ expect(wrapper.emitted('sorting:changed')).toEqual([
+ [{ orderBy: defaultProps.sortableFields[0].orderBy }],
+ ]);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js b/spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js
index 3a5514ef318..1fce3c5d0b0 100644
--- a/spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js
+++ b/spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js
@@ -1,6 +1,6 @@
-import Vue from 'vue';
import { mount } from '@vue/test-utils';
import $ from 'jquery';
+import Vue from 'vue';
import ResizableChartContainer from '~/vue_shared/components/resizable_chart/resizable_chart_container.vue';
jest.mock('~/lib/utils/common_utils', () => ({
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/editor_service_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/editor_service_spec.js
index 51619cd9578..ce2b0d1ddc1 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/editor_service_spec.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/editor_service_spec.js
@@ -1,3 +1,5 @@
+import buildCustomRenderer from '~/vue_shared/components/rich_content_editor/services/build_custom_renderer';
+import buildHTMLToMarkdownRenderer from '~/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer';
import {
generateToolbarItem,
addCustomEventListener,
@@ -8,8 +10,6 @@ import {
getMarkdown,
getEditorOptions,
} from '~/vue_shared/components/rich_content_editor/services/editor_service';
-import buildHTMLToMarkdownRenderer from '~/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer';
-import buildCustomRenderer from '~/vue_shared/components/rich_content_editor/services/build_custom_renderer';
import sanitizeHTML from '~/vue_shared/components/rich_content_editor/services/sanitize_html';
jest.mock('~/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer');
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal_spec.js
index 16370a7aaad..97aecda97d2 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal_spec.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal_spec.js
@@ -1,8 +1,8 @@
-import { shallowMount } from '@vue/test-utils';
import { GlModal, GlTabs } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { IMAGE_TABS } from '~/vue_shared/components/rich_content_editor/constants';
import AddImageModal from '~/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue';
import UploadImageTab from '~/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab.vue';
-import { IMAGE_TABS } from '~/vue_shared/components/rich_content_editor/constants';
describe('Add Image Modal', () => {
let wrapper;
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/modals/insert_video_modal_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/modals/insert_video_modal_spec.js
index d59d4cc1de9..3e9eaf58181 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/modals/insert_video_modal_spec.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/modals/insert_video_modal_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import InsertVideoModal from '~/vue_shared/components/rich_content_editor/modals/insert_video_modal.vue';
describe('Insert Video Modal', () => {
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_integration_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_integration_spec.js
index b9b93b274d2..47b1abd2ad2 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_integration_spec.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_integration_spec.js
@@ -1,6 +1,6 @@
import Editor from '@toast-ui/editor';
-import { registerHTMLToMarkdownRenderer } from '~/vue_shared/components/rich_content_editor/services/editor_service';
import buildMarkdownToHTMLRenderer from '~/vue_shared/components/rich_content_editor/services/build_custom_renderer';
+import { registerHTMLToMarkdownRenderer } from '~/vue_shared/components/rich_content_editor/services/editor_service';
describe('vue_shared/components/rich_content_editor', () => {
let editor;
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js
index 2eb353a1801..8eb880b3984 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js
@@ -1,14 +1,14 @@
-import { shallowMount } from '@vue/test-utils';
import { Editor, mockEditorApi } from '@toast-ui/vue-editor';
-import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
-import AddImageModal from '~/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue';
-import InsertVideoModal from '~/vue_shared/components/rich_content_editor/modals/insert_video_modal.vue';
+import { shallowMount } from '@vue/test-utils';
import {
EDITOR_TYPES,
EDITOR_HEIGHT,
EDITOR_PREVIEW_STYLE,
CUSTOM_EVENTS,
} from '~/vue_shared/components/rich_content_editor/constants';
+import AddImageModal from '~/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue';
+import InsertVideoModal from '~/vue_shared/components/rich_content_editor/modals/insert_video_modal.vue';
+import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
import {
addCustomEventListener,
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_font_awesome_html_inline_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_font_awesome_html_inline_spec.js
index d6bb01259bb..c1aaed6f0c3 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_font_awesome_html_inline_spec.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_font_awesome_html_inline_spec.js
@@ -1,5 +1,5 @@
-import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_font_awesome_html_inline';
import { buildUneditableInlineTokens } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
+import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_font_awesome_html_inline';
import { normalTextNode } from './mock_data';
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_html_block_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_html_block_spec.js
index b31684a400e..234f6a4d4ca 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_html_block_spec.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_html_block_spec.js
@@ -1,5 +1,5 @@
-import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_html_block';
import { buildUneditableHtmlAsTextTokens } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
+import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_html_block';
describe('rich_content_editor/services/renderers/render_html_block', () => {
const htmlBlockNode = {
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text_spec.js
index 521885f5687..425d0f41bcd 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text_spec.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text_spec.js
@@ -1,5 +1,5 @@
-import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text';
import { buildUneditableInlineTokens } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
+import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text';
import { buildMockTextNode, normalTextNode } from './mock_data';
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js
index 774f830f421..7c1809c290c 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js
@@ -1,15 +1,14 @@
import {
+ buildUneditableBlockTokens,
+ buildUneditableOpenTokens,
+} from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
+import {
renderUneditableLeaf,
renderUneditableBranch,
renderWithAttributeDefinitions,
willAlwaysRender,
} from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils';
-import {
- buildUneditableBlockTokens,
- buildUneditableOpenTokens,
-} from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
-
import { originToken, uneditableCloseToken, attributeDefinition } from './mock_data';
describe('rich_content_editor/renderers/render_utils', () => {
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/toolbar_item_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/toolbar_item_spec.js
index 0e6f951bd53..5a56b499769 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/toolbar_item_spec.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/toolbar_item_spec.js
@@ -1,6 +1,6 @@
+import { GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import { GlIcon } from '@gitlab/ui';
import ToolbarItem from '~/vue_shared/components/rich_content_editor/toolbar_item.vue';
describe('Toolbar Item', () => {
diff --git a/spec/frontend/vue_shared/components/runner_instructions/mock_data.js b/spec/frontend/vue_shared/components/runner_instructions/mock_data.js
new file mode 100644
index 00000000000..01f7f3d49c7
--- /dev/null
+++ b/spec/frontend/vue_shared/components/runner_instructions/mock_data.js
@@ -0,0 +1,107 @@
+export const mockGraphqlRunnerPlatforms = {
+ data: {
+ runnerPlatforms: {
+ nodes: [
+ {
+ name: 'linux',
+ humanReadableName: 'Linux',
+ architectures: {
+ nodes: [
+ {
+ name: 'amd64',
+ downloadLocation:
+ 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64',
+ __typename: 'RunnerArchitecture',
+ },
+ {
+ name: '386',
+ downloadLocation:
+ 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-386',
+ __typename: 'RunnerArchitecture',
+ },
+ {
+ name: 'arm',
+ downloadLocation:
+ 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-arm',
+ __typename: 'RunnerArchitecture',
+ },
+ {
+ name: 'arm64',
+ downloadLocation:
+ 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-arm64',
+ __typename: 'RunnerArchitecture',
+ },
+ ],
+ __typename: 'RunnerArchitectureConnection',
+ },
+ __typename: 'RunnerPlatform',
+ },
+ {
+ name: 'osx',
+ humanReadableName: 'macOS',
+ architectures: {
+ nodes: [
+ {
+ name: 'amd64',
+ downloadLocation:
+ 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-darwin-amd64',
+ __typename: 'RunnerArchitecture',
+ },
+ ],
+ __typename: 'RunnerArchitectureConnection',
+ },
+ __typename: 'RunnerPlatform',
+ },
+ {
+ name: 'windows',
+ humanReadableName: 'Windows',
+ architectures: {
+ nodes: [
+ {
+ name: 'amd64',
+ downloadLocation:
+ 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-windows-amd64.exe',
+ __typename: 'RunnerArchitecture',
+ },
+ {
+ name: '386',
+ downloadLocation:
+ 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-windows-386.exe',
+ __typename: 'RunnerArchitecture',
+ },
+ ],
+ __typename: 'RunnerArchitectureConnection',
+ },
+ __typename: 'RunnerPlatform',
+ },
+ {
+ name: 'docker',
+ humanReadableName: 'Docker',
+ architectures: null,
+ __typename: 'RunnerPlatform',
+ },
+ {
+ name: 'kubernetes',
+ humanReadableName: 'Kubernetes',
+ architectures: null,
+ __typename: 'RunnerPlatform',
+ },
+ ],
+ __typename: 'RunnerPlatformConnection',
+ },
+ project: { id: 'gid://gitlab/Project/1', __typename: 'Project' },
+ group: null,
+ },
+};
+
+export const mockGraphqlInstructions = {
+ data: {
+ runnerSetup: {
+ installInstructions:
+ "# Download the binary for your system\nsudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64\n\n# Give it permissions to execute\nsudo chmod +x /usr/local/bin/gitlab-runner\n\n# Create a GitLab CI user\nsudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash\n\n# Install and run as service\nsudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner\nsudo gitlab-runner start\n",
+ registerInstructions:
+ 'sudo gitlab-runner register --url http://192.168.1.81:3000/ --registration-token GE5gsjeep_HAtBf9s3Yz',
+ __typename: 'RunnerSetup',
+ },
+ },
+};
diff --git a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js
new file mode 100644
index 00000000000..48db60bfd33
--- /dev/null
+++ b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js
@@ -0,0 +1,113 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import getRunnerPlatforms from '~/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql';
+import getRunnerSetupInstructions from '~/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql';
+import RunnerInstructions from '~/vue_shared/components/runner_instructions/runner_instructions.vue';
+
+import { mockGraphqlRunnerPlatforms, mockGraphqlInstructions } from './mock_data';
+
+const projectPath = 'gitlab-org/gitlab';
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+describe('RunnerInstructions component', () => {
+ let wrapper;
+ let fakeApollo;
+
+ const findModalButton = () => wrapper.find('[data-testid="show-modal-button"]');
+ const findPlatformButtons = () => wrapper.findAll('[data-testid="platform-button"]');
+ const findArchitectureDropdownItems = () =>
+ wrapper.findAll('[data-testid="architecture-dropdown-item"]');
+ const findBinaryInstructionsSection = () => wrapper.find('[data-testid="binary-instructions"]');
+ const findRunnerInstructionsSection = () => wrapper.find('[data-testid="runner-instructions"]');
+
+ beforeEach(async () => {
+ const requestHandlers = [
+ [getRunnerPlatforms, jest.fn().mockResolvedValue(mockGraphqlRunnerPlatforms)],
+ [getRunnerSetupInstructions, jest.fn().mockResolvedValue(mockGraphqlInstructions)],
+ ];
+
+ fakeApollo = createMockApollo(requestHandlers);
+
+ wrapper = shallowMount(RunnerInstructions, {
+ provide: {
+ projectPath,
+ },
+ localVue,
+ apolloProvider: fakeApollo,
+ });
+
+ await wrapper.vm.$nextTick();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('should show the "Show Runner installation instructions" button', () => {
+ const button = findModalButton();
+
+ expect(button.exists()).toBe(true);
+ expect(button.text()).toBe('Show Runner installation instructions');
+ });
+
+ it('should contain a number of platforms buttons', () => {
+ const buttons = findPlatformButtons();
+
+ expect(buttons).toHaveLength(mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes.length);
+ });
+
+ it('should contain a number of dropdown items for the architecture options', () => {
+ const platformButton = findPlatformButtons().at(0);
+ platformButton.vm.$emit('click');
+
+ return wrapper.vm.$nextTick(() => {
+ const dropdownItems = findArchitectureDropdownItems();
+
+ expect(dropdownItems).toHaveLength(
+ mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes[0].architectures.nodes.length,
+ );
+ });
+ });
+
+ it('should display the binary installation instructions for a selected architecture', async () => {
+ const platformButton = findPlatformButtons().at(0);
+ platformButton.vm.$emit('click');
+
+ await wrapper.vm.$nextTick();
+
+ const dropdownItem = findArchitectureDropdownItems().at(0);
+ dropdownItem.vm.$emit('click');
+
+ await wrapper.vm.$nextTick();
+
+ const runner = findBinaryInstructionsSection();
+
+ expect(runner.text()).toMatch('sudo chmod +x /usr/local/bin/gitlab-runner');
+ expect(runner.text()).toMatch(
+ `sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash`,
+ );
+ expect(runner.text()).toMatch(
+ 'sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner',
+ );
+ expect(runner.text()).toMatch('sudo gitlab-runner start');
+ });
+
+ it('should display the runner register instructions for a selected architecture', async () => {
+ const platformButton = findPlatformButtons().at(0);
+ platformButton.vm.$emit('click');
+
+ await wrapper.vm.$nextTick();
+
+ const dropdownItem = findArchitectureDropdownItems().at(0);
+ dropdownItem.vm.$emit('click');
+
+ await wrapper.vm.$nextTick();
+
+ const runner = findRunnerInstructionsSection();
+
+ expect(runner.text()).toMatch(mockGraphqlInstructions.data.runnerSetup.registerInstructions);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap b/spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap
new file mode 100644
index 00000000000..51b8aa162bc
--- /dev/null
+++ b/spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap
@@ -0,0 +1,43 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Settings Block renders the correct markup 1`] = `
+<section
+ class="settings no-animate"
+>
+ <div
+ class="settings-header"
+ >
+ <h4>
+ <div
+ data-testid="title-slot"
+ />
+ </h4>
+
+ <gl-button-stub
+ buttontextclasses=""
+ category="primary"
+ icon=""
+ size="medium"
+ variant="default"
+ >
+
+ Expand
+
+ </gl-button-stub>
+
+ <p>
+ <div
+ data-testid="description-slot"
+ />
+ </p>
+ </div>
+
+ <div
+ class="settings-content"
+ >
+ <div
+ data-testid="default-slot"
+ />
+ </div>
+</section>
+`;
diff --git a/spec/frontend/vue_shared/components/settings/settings_block_spec.js b/spec/frontend/vue_shared/components/settings/settings_block_spec.js
new file mode 100644
index 00000000000..2db0b001b5b
--- /dev/null
+++ b/spec/frontend/vue_shared/components/settings/settings_block_spec.js
@@ -0,0 +1,86 @@
+import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import component from '~/vue_shared/components/settings/settings_block.vue';
+
+describe('Settings Block', () => {
+ let wrapper;
+
+ const mountComponent = (propsData) => {
+ wrapper = shallowMount(component, {
+ propsData,
+ slots: {
+ title: '<div data-testid="title-slot"></div>',
+ description: '<div data-testid="description-slot"></div>',
+ default: '<div data-testid="default-slot"></div>',
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findDefaultSlot = () => wrapper.find('[data-testid="default-slot"]');
+ const findTitleSlot = () => wrapper.find('[data-testid="title-slot"]');
+ const findDescriptionSlot = () => wrapper.find('[data-testid="description-slot"]');
+ const findExpandButton = () => wrapper.find(GlButton);
+
+ it('renders the correct markup', () => {
+ mountComponent();
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('has a default slot', () => {
+ mountComponent();
+
+ expect(findDefaultSlot().exists()).toBe(true);
+ });
+
+ it('has a title slot', () => {
+ mountComponent();
+
+ expect(findTitleSlot().exists()).toBe(true);
+ });
+
+ it('has a description slot', () => {
+ mountComponent();
+
+ expect(findDescriptionSlot().exists()).toBe(true);
+ });
+
+ describe('expanded behaviour', () => {
+ it('is collapsed by default', () => {
+ mountComponent();
+
+ expect(wrapper.classes('expanded')).toBe(false);
+ });
+
+ it('adds expanded class when the expand button is clicked', async () => {
+ mountComponent();
+
+ expect(wrapper.classes('expanded')).toBe(false);
+ expect(findExpandButton().text()).toBe('Expand');
+
+ await findExpandButton().vm.$emit('click');
+
+ expect(wrapper.classes('expanded')).toBe(true);
+ expect(findExpandButton().text()).toBe('Collapse');
+ });
+
+ it('is expanded when `defaultExpanded` is true no matter what', async () => {
+ mountComponent({ defaultExpanded: true });
+
+ expect(wrapper.classes('expanded')).toBe(true);
+
+ await findExpandButton().vm.$emit('click');
+
+ expect(wrapper.classes('expanded')).toBe(true);
+
+ await findExpandButton().vm.$emit('click');
+
+ expect(wrapper.classes('expanded')).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js b/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js
index fc1fa3fc1c1..3221e88192b 100644
--- a/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js
@@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
-import SidebarDatePicker from '~/vue_shared/components/sidebar/date_picker.vue';
import DatePicker from '~/vue_shared/components/pikaday.vue';
+import SidebarDatePicker from '~/vue_shared/components/sidebar/date_picker.vue';
describe('SidebarDatePicker', () => {
let wrapper;
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 256b3cff525..a5a099d803a 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
@@ -1,5 +1,3 @@
-import { shallowMount } from '@vue/test-utils';
-import MockAdapter from 'axios-mock-adapter';
import {
GlIcon,
GlLoadingIcon,
@@ -9,6 +7,8 @@ import {
GlSearchBoxByType,
GlButton,
} from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import IssuableMoveDropdown from '~/vue_shared/components/sidebar/issuable_move_dropdown.vue';
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js
index a55ad37c498..68ea94e72ce 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js
@@ -1,6 +1,6 @@
+import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
-import { shallowMount } from '@vue/test-utils';
import LabelsSelect from '~/labels_select';
import BaseComponent from '~/vue_shared/components/sidebar/labels_select/base.vue';
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
index 4b4d265800b..322e632da02 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
@@ -79,7 +79,7 @@ describe('DropdownCreateLabelComponent', () => {
const colorItemEl = colorsListContainerEl.querySelectorAll('a')[0];
expect(colorItemEl.dataset.color).toBe(vm.suggestedColors[0].colorCode);
- expect(colorItemEl.getAttribute('style')).toBe('background-color: rgb(0, 51, 204);');
+ expect(colorItemEl.getAttribute('style')).toBe('background-color: rgb(0, 153, 102);');
});
it('renders color input element', () => {
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js
index 5cbbb99eaef..30dd92b72a4 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import dropdownTitleComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_title.vue';
const createComponent = (canEdit = true) =>
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
index ecb3c3a42c8..37f59c108df 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
@@ -1,5 +1,5 @@
-import { mount } from '@vue/test-utils';
import { GlLabel } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import DropdownValueComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value.vue';
import { mockConfig, mockLabels } from './mock_data';
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js b/spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js
index 648ba84fe8f..73716d4edf3 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js
@@ -16,27 +16,27 @@ export const mockLabels = [
];
export const mockSuggestedColors = {
- '#0033CC': 'UA blue',
- '#428BCA': 'Moderate blue',
- '#44AD8E': 'Lime green',
- '#A8D695': 'Feijoa',
- '#5CB85C': 'Slightly desaturated green',
- '#69D100': 'Bright green',
- '#004E00': 'Very dark lime green',
- '#34495E': 'Very dark desaturated blue',
- '#7F8C8D': 'Dark grayish cyan',
- '#A295D6': 'Slightly desaturated blue',
- '#5843AD': 'Dark moderate blue',
- '#8E44AD': 'Dark moderate violet',
- '#FFECDB': 'Very pale orange',
- '#AD4363': 'Dark moderate pink',
- '#D10069': 'Strong pink',
- '#CC0033': 'Strong red',
- '#FF0000': 'Pure red',
- '#D9534F': 'Soft red',
- '#D1D100': 'Strong yellow',
- '#F0AD4E': 'Soft orange',
- '#AD8D43': 'Dark moderate orange',
+ '#009966': 'Green-cyan',
+ '#8fbc8f': 'Dark sea green',
+ '#3cb371': 'Medium sea green',
+ '#00b140': 'Green screen',
+ '#013220': 'Dark green',
+ '#6699cc': 'Blue-gray',
+ '#0000ff': 'Blue',
+ '#e6e6fa': 'Lavendar',
+ '#9400d3': 'Dark violet',
+ '#330066': 'Deep violet',
+ '#808080': 'Gray',
+ '#36454f': 'Charcoal grey',
+ '#f7e7ce': 'Champagne',
+ '#c21e56': 'Rose red',
+ '#cc338b': 'Magenta-pink',
+ '#dc143c': 'Crimson',
+ '#ff0000': 'Red',
+ '#cd5b45': 'Dark coral',
+ '#eee600': 'Titanium yellow',
+ '#ed9121': 'Carrot orange',
+ '#c39953': 'Aztec Gold',
};
export const mockConfig = {
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js
index 951f706421f..59b170bfba9 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js
@@ -1,7 +1,7 @@
-import Vuex from 'vuex';
+import { GlIcon, GlButton } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
-import { GlIcon, GlButton } from '@gitlab/ui';
import DropdownButton from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue';
import labelSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js
index 0f49fe4fc5b..c4a645082e6 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js
@@ -1,7 +1,7 @@
-import Vuex from 'vuex';
+import { GlButton, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
-import { GlButton, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui';
import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue';
import labelSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js
index 989cd256e26..60903933505 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js
@@ -1,6 +1,3 @@
-import Vuex from 'vuex';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-
import {
GlIntersectionObserver,
GlButton,
@@ -8,14 +5,16 @@ import {
GlSearchBoxByType,
GlLink,
} from '@gitlab/ui';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue';
import LabelItem from '~/vue_shared/components/sidebar/labels_select_vue/label_item.vue';
-import defaultState from '~/vue_shared/components/sidebar/labels_select_vue/store/state';
-import mutations from '~/vue_shared/components/sidebar/labels_select_vue/store/mutations';
import * as actions from '~/vue_shared/components/sidebar/labels_select_vue/store/actions';
import * as getters from '~/vue_shared/components/sidebar/labels_select_vue/store/getters';
+import mutations from '~/vue_shared/components/sidebar/labels_select_vue/store/mutations';
+import defaultState from '~/vue_shared/components/sidebar/labels_select_vue/store/state';
import { mockConfig, mockLabels, mockRegularLabel } from './mock_data';
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_spec.js
index 97946993857..1175d183c6c 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_spec.js
@@ -1,5 +1,5 @@
-import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
import DropdownContents from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue';
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js
index c1d9be7393c..726a113dbd9 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js
@@ -1,7 +1,7 @@
-import Vuex from 'vuex';
+import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
-import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import DropdownTitle from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue';
import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_spec.js
index 70311f8235f..0d1d6ebcfe5 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_spec.js
@@ -1,7 +1,7 @@
-import Vuex from 'vuex';
+import { GlLabel } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
-import { GlLabel } from '@gitlab/ui';
import DropdownValue from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue';
import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js
index a6ec01ad7e1..bd1705e7693 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js
@@ -1,6 +1,6 @@
+import { GlIcon, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { GlIcon, GlLink } from '@gitlab/ui';
import LabelItem from '~/vue_shared/components/sidebar/labels_select_vue/label_item.vue';
import { mockRegularLabel } from './mock_data';
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js
index 1206450bbeb..4cf36df2502 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js
@@ -1,15 +1,15 @@
-import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
-import LabelsSelectRoot from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
-import DropdownTitle from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue';
-import DropdownValue from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue';
+import { isInViewport } from '~/lib/utils/common_utils';
import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue';
import DropdownButton from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue';
import DropdownContents from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue';
+import DropdownTitle from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue';
+import DropdownValue from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue';
+import LabelsSelectRoot from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
-import { isInViewport } from '~/lib/utils/common_utils';
import { mockConfig } from './mock_data';
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js
index 9697d6c30f2..85a14226585 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js
@@ -50,25 +50,25 @@ export const mockConfig = {
};
export const mockSuggestedColors = {
- '#0033CC': 'UA blue',
- '#428BCA': 'Moderate blue',
- '#44AD8E': 'Lime green',
- '#A8D695': 'Feijoa',
- '#5CB85C': 'Slightly desaturated green',
- '#69D100': 'Bright green',
- '#004E00': 'Very dark lime green',
- '#34495E': 'Very dark desaturated blue',
- '#7F8C8D': 'Dark grayish cyan',
- '#A295D6': 'Slightly desaturated blue',
- '#5843AD': 'Dark moderate blue',
- '#8E44AD': 'Dark moderate violet',
- '#FFECDB': 'Very pale orange',
- '#AD4363': 'Dark moderate pink',
- '#D10069': 'Strong pink',
- '#CC0033': 'Strong red',
- '#FF0000': 'Pure red',
- '#D9534F': 'Soft red',
- '#D1D100': 'Strong yellow',
- '#F0AD4E': 'Soft orange',
- '#AD8D43': 'Dark moderate orange',
+ '#009966': 'Green-cyan',
+ '#8fbc8f': 'Dark sea green',
+ '#3cb371': 'Medium sea green',
+ '#00b140': 'Green screen',
+ '#013220': 'Dark green',
+ '#6699cc': 'Blue-gray',
+ '#0000ff': 'Blue',
+ '#e6e6fa': 'Lavendar',
+ '#9400d3': 'Dark violet',
+ '#330066': 'Deep violet',
+ '#808080': 'Gray',
+ '#36454f': 'Charcoal grey',
+ '#f7e7ce': 'Champagne',
+ '#c21e56': 'Rose red',
+ '#cc338b': 'Magenta-pink',
+ '#dc143c': 'Crimson',
+ '#ff0000': 'Red',
+ '#cd5b45': 'Dark coral',
+ '#eee600': 'Titanium yellow',
+ '#ed9121': 'Carrot orange',
+ '#c39953': 'Aztec Gold',
};
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js
index 4909c43bc96..3f11095cb04 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js
@@ -1,11 +1,10 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import defaultState from '~/vue_shared/components/sidebar/labels_select_vue/store/state';
-import * as types from '~/vue_shared/components/sidebar/labels_select_vue/store/mutation_types';
-import * as actions from '~/vue_shared/components/sidebar/labels_select_vue/store/actions';
-
import axios from '~/lib/utils/axios_utils';
+import * as actions from '~/vue_shared/components/sidebar/labels_select_vue/store/actions';
+import * as types from '~/vue_shared/components/sidebar/labels_select_vue/store/mutation_types';
+import defaultState from '~/vue_shared/components/sidebar/labels_select_vue/store/state';
describe('LabelsSelect Actions', () => {
let state;
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js
index 208f2f2d42d..ab266ac8aed 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js
@@ -1,5 +1,5 @@
-import mutations from '~/vue_shared/components/sidebar/labels_select_vue/store/mutations';
import * as types from '~/vue_shared/components/sidebar/labels_select_vue/store/mutation_types';
+import mutations from '~/vue_shared/components/sidebar/labels_select_vue/store/mutations';
describe('LabelsSelect Mutations', () => {
describe(`${types.SET_INITIAL_STATE}`, () => {
diff --git a/spec/frontend/vue_shared/components/smart_virtual_list_spec.js b/spec/frontend/vue_shared/components/smart_virtual_list_spec.js
index e5f9b94128e..8802a832781 100644
--- a/spec/frontend/vue_shared/components/smart_virtual_list_spec.js
+++ b/spec/frontend/vue_shared/components/smart_virtual_list_spec.js
@@ -1,5 +1,5 @@
-import Vue from 'vue';
import { mount } from '@vue/test-utils';
+import Vue from 'vue';
import SmartVirtualScrollList from '~/vue_shared/components/smart_virtual_list.vue';
describe('Toggle Button', () => {
diff --git a/spec/frontend/vue_shared/components/table_pagination_spec.js b/spec/frontend/vue_shared/components/table_pagination_spec.js
index 12c47637358..ed23a47c328 100644
--- a/spec/frontend/vue_shared/components/table_pagination_spec.js
+++ b/spec/frontend/vue_shared/components/table_pagination_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlPagination } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
describe('Pagination component', () => {
diff --git a/spec/frontend/vue_shared/components/tabs/tabs_spec.js b/spec/frontend/vue_shared/components/tabs/tabs_spec.js
index 49d92094b34..fe7be5be899 100644
--- a/spec/frontend/vue_shared/components/tabs/tabs_spec.js
+++ b/spec/frontend/vue_shared/components/tabs/tabs_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import Tabs from '~/vue_shared/components/tabs/tabs';
import Tab from '~/vue_shared/components/tabs/tab.vue';
+import Tabs from '~/vue_shared/components/tabs/tabs';
describe('Tabs component', () => {
let vm;
diff --git a/spec/frontend/vue_shared/components/todo_button_spec.js b/spec/frontend/vue_shared/components/todo_button_spec.js
index 1f8a214d632..8043bb7785b 100644
--- a/spec/frontend/vue_shared/components/todo_button_spec.js
+++ b/spec/frontend/vue_shared/components/todo_button_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount, mount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
+import { shallowMount, mount } from '@vue/test-utils';
import TodoButton from '~/vue_shared/components/todo_button.vue';
describe('Todo Button', () => {
@@ -33,7 +33,7 @@ describe('Todo Button', () => {
it.each`
label | isTodo
${'Mark as done'} | ${true}
- ${'Add a To Do'} | ${false}
+ ${'Add a to do'} | ${false}
`('sets correct label when isTodo is $isTodo', ({ label, isTodo }) => {
createComponent({ isTodo });
diff --git a/spec/frontend/vue_shared/components/toggle_button_spec.js b/spec/frontend/vue_shared/components/toggle_button_spec.js
deleted file mode 100644
index 2822b1999bc..00000000000
--- a/spec/frontend/vue_shared/components/toggle_button_spec.js
+++ /dev/null
@@ -1,96 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlIcon } from '@gitlab/ui';
-import ToggleButton from '~/vue_shared/components/toggle_button.vue';
-
-describe('Toggle Button component', () => {
- let wrapper;
-
- function createComponent(propsData = {}) {
- wrapper = shallowMount(ToggleButton, {
- propsData,
- });
- }
-
- const findInput = () => wrapper.find('input');
- const findButton = () => wrapper.find('button');
- const findToggleIcon = () => wrapper.find(GlIcon);
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- it('renders input with provided name', () => {
- createComponent({
- name: 'foo',
- });
-
- expect(findInput().attributes('name')).toBe('foo');
- });
-
- describe.each`
- value | iconName
- ${true} | ${'status_success_borderless'}
- ${false} | ${'status_failed_borderless'}
- `('when `value` prop is `$value`', ({ value, iconName }) => {
- beforeEach(() => {
- createComponent({
- value,
- name: 'foo',
- });
- });
-
- it('renders input with correct value attribute', () => {
- expect(findInput().attributes('value')).toBe(`${value}`);
- });
-
- it('renders correct icon', () => {
- const icon = findToggleIcon();
- expect(icon.isVisible()).toBe(true);
- expect(icon.props('name')).toBe(iconName);
- expect(findButton().classes('is-checked')).toBe(value);
- });
-
- describe('when clicked', () => {
- it('emits `change` event with correct event', async () => {
- findButton().trigger('click');
- await wrapper.vm.$nextTick();
-
- expect(wrapper.emitted('change')).toStrictEqual([[!value]]);
- });
- });
- });
-
- describe('when `disabledInput` prop is `true`', () => {
- beforeEach(() => {
- createComponent({
- value: true,
- disabledInput: true,
- });
- });
-
- it('renders disabled button', () => {
- expect(findButton().classes()).toContain('is-disabled');
- });
-
- it('does not emit change event when clicked', async () => {
- findButton().trigger('click');
- await wrapper.vm.$nextTick();
-
- expect(wrapper.emitted('change')).toBeFalsy();
- });
- });
-
- describe('when `isLoading` prop is `true`', () => {
- beforeEach(() => {
- createComponent({
- value: true,
- isLoading: true,
- });
- });
-
- it('renders loading class', () => {
- expect(findButton().classes()).toContain('is-loading');
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js b/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js
index 11982eb513d..ace486b1f32 100644
--- a/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js
+++ b/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
jest.mock('~/flash');
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js
index d151cd15bc4..d62c4a98b10 100644
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js
+++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js
@@ -1,10 +1,10 @@
+import { GlLink } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { each } from 'lodash';
import { trimText } from 'helpers/text_helper';
-import { shallowMount } from '@vue/test-utils';
-import { GlLink } from '@gitlab/ui';
import { TEST_HOST } from 'spec/test_constants';
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
describe('User Avatar Link Component', () => {
let wrapper;
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js
index e3cd2bb9aaa..1d15da491cd 100644
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js
+++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js
@@ -1,8 +1,8 @@
-import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'spec/test_constants';
-import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue';
const TEST_IMAGE_SIZE = 7;
const TEST_BREAKPOINT = 5;
diff --git a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
index 435c3a5406e..a6c5e23ae14 100644
--- a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
+++ b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
@@ -1,8 +1,8 @@
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlSprintf, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import UserPopover from '~/vue_shared/components/user_popover/user_popover.vue';
-import UserAvailabilityStatus from '~/set_status_modal/components/user_availability_status.vue';
import { AVAILABILITY_STATUS } from '~/set_status_modal/utils';
+import UserNameWithStatus from '~/sidebar/components/assignees/user_name_with_status.vue';
+import UserPopover from '~/vue_shared/components/user_popover/user_popover.vue';
const DEFAULT_PROPS = {
user: {
@@ -36,7 +36,7 @@ describe('User Popover Component', () => {
const findByTestId = (testid) => wrapper.find(`[data-testid="${testid}"]`);
const findUserStatus = () => wrapper.find('.js-user-status');
const findTarget = () => document.querySelector('.js-user-link');
- const findAvailabilityStatus = () => wrapper.find(UserAvailabilityStatus);
+ const findUserName = () => wrapper.find(UserNameWithStatus);
const createWrapper = (props = {}, options = {}) => {
wrapper = shallowMount(UserPopover, {
@@ -47,7 +47,7 @@ describe('User Popover Component', () => {
},
stubs: {
GlSprintf,
- UserAvailabilityStatus,
+ UserNameWithStatus,
},
...options,
});
@@ -213,7 +213,7 @@ describe('User Popover Component', () => {
createWrapper({ user });
- expect(findAvailabilityStatus().exists()).toBe(true);
+ expect(findUserName().exists()).toBe(true);
expect(wrapper.text()).toContain(user.name);
expect(wrapper.text()).toContain('(Busy)');
});
diff --git a/spec/frontend/vue_shared/components/web_ide_link_spec.js b/spec/frontend/vue_shared/components/web_ide_link_spec.js
index 8ed072bed13..eb23a8ef457 100644
--- a/spec/frontend/vue_shared/components/web_ide_link_spec.js
+++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
+import ActionsButton from '~/vue_shared/components/actions_button.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
-import ActionsButton from '~/vue_shared/components/actions_button.vue';
const TEST_EDIT_URL = '/gitlab-test/test/-/edit/master/';
const TEST_WEB_IDE_URL = '/-/ide/project/gitlab-test/test/edit/master/-/';
diff --git a/spec/frontend/vue_shared/directives/tooltip_spec.js b/spec/frontend/vue_shared/directives/tooltip_spec.js
index 28ec23ad4c1..99e8b5b552b 100644
--- a/spec/frontend/vue_shared/directives/tooltip_spec.js
+++ b/spec/frontend/vue_shared/directives/tooltip_spec.js
@@ -1,6 +1,6 @@
+import { mount } from '@vue/test-utils';
import $ from 'jquery';
import { escape } from 'lodash';
-import { mount } from '@vue/test-utils';
import tooltip from '~/vue_shared/directives/tooltip';
const DEFAULT_TOOLTIP_TEMPLATE = '<div v-tooltip :title="tooltip"></div>';
diff --git a/spec/frontend/vue_shared/directives/track_event_spec.js b/spec/frontend/vue_shared/directives/track_event_spec.js
index 8d867c8e3fc..d7d7f4edc3f 100644
--- a/spec/frontend/vue_shared/directives/track_event_spec.js
+++ b/spec/frontend/vue_shared/directives/track_event_spec.js
@@ -1,11 +1,11 @@
-import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
import Tracking from '~/tracking';
import TrackEvent from '~/vue_shared/directives/track_event';
jest.mock('~/tracking');
-const Component = Vue.component('dummy-element', {
+const Component = Vue.component('DummyElement', {
directives: {
TrackEvent,
},
diff --git a/spec/frontend/vue_shared/security_reports/mock_data.js b/spec/frontend/vue_shared/security_reports/mock_data.js
index b3ff7daef2b..7918f70d702 100644
--- a/spec/frontend/vue_shared/security_reports/mock_data.js
+++ b/spec/frontend/vue_shared/security_reports/mock_data.js
@@ -322,6 +322,23 @@ export const secretScanningDiffSuccessMock = {
head_report_created_at: '2020-01-10T10:00:00.000Z',
};
+export const securityReportDownloadPathsQueryNoArtifactsResponse = {
+ project: {
+ mergeRequest: {
+ headPipeline: {
+ id: 'gid://gitlab/Ci::Pipeline/176',
+ jobs: {
+ nodes: [],
+ __typename: 'CiJobConnection',
+ },
+ __typename: 'Pipeline',
+ },
+ __typename: 'MergeRequest',
+ },
+ __typename: 'Project',
+ },
+};
+
export const securityReportDownloadPathsQueryResponse = {
project: {
mergeRequest: {
diff --git a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js b/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
index 50d1d130675..0b4816a951e 100644
--- a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
+++ b/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
@@ -1,6 +1,7 @@
-import { mount, createLocalVue } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { merge } from 'lodash';
+import Vue from 'vue';
import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -8,26 +9,26 @@ import { trimText } from 'helpers/text_helper';
import waitForPromises from 'helpers/wait_for_promises';
import {
expectedDownloadDropdownProps,
+ securityReportDownloadPathsQueryNoArtifactsResponse,
securityReportDownloadPathsQueryResponse,
sastDiffSuccessMock,
secretScanningDiffSuccessMock,
} from 'jest/vue_shared/security_reports/mock_data';
-import Api from '~/api';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import HelpIcon from '~/vue_shared/security_reports/components/help_icon.vue';
+import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue';
import {
REPORT_TYPE_SAST,
REPORT_TYPE_SECRET_DETECTION,
} from '~/vue_shared/security_reports/constants';
-import HelpIcon from '~/vue_shared/security_reports/components/help_icon.vue';
-import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue';
-import SecurityReportsApp from '~/vue_shared/security_reports/security_reports_app.vue';
import securityReportDownloadPathsQuery from '~/vue_shared/security_reports/queries/security_report_download_paths.query.graphql';
+import SecurityReportsApp from '~/vue_shared/security_reports/security_reports_app.vue';
jest.mock('~/flash');
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(VueApollo);
+Vue.use(Vuex);
const SAST_COMPARISON_PATH = '/sast.json';
const SECRET_SCANNING_COMPARISON_PATH = '/secret_detection.json';
@@ -47,7 +48,6 @@ describe('Security reports app', () => {
SecurityReportsApp,
merge(
{
- localVue,
propsData: { ...props },
stubs: {
HelpIcon: true,
@@ -60,187 +60,94 @@ describe('Security reports app', () => {
const pendingHandler = () => new Promise(() => {});
const successHandler = () => Promise.resolve({ data: securityReportDownloadPathsQueryResponse });
+ const successEmptyHandler = () =>
+ Promise.resolve({ data: securityReportDownloadPathsQueryNoArtifactsResponse });
const failureHandler = () => Promise.resolve({ errors: [{ message: 'some error' }] });
const createMockApolloProvider = (handler) => {
- localVue.use(VueApollo);
-
const requestHandlers = [[securityReportDownloadPathsQuery, handler]];
return createMockApollo(requestHandlers);
};
- const anyParams = expect.any(Object);
-
const findDownloadDropdown = () => wrapper.find(SecurityReportDownloadDropdown);
- const findPipelinesTabAnchor = () => wrapper.find('[data-testid="show-pipelines"]');
const findHelpIconComponent = () => wrapper.find(HelpIcon);
- const setupMockJobArtifact = (reportType) => {
- jest
- .spyOn(Api, 'pipelineJobs')
- .mockResolvedValue({ data: [{ artifacts: [{ file_type: reportType }] }] });
- };
- const expectPipelinesTabAnchor = () => {
- const mrTabsMock = { tabShown: jest.fn() };
- window.mrTabs = mrTabsMock;
- findPipelinesTabAnchor().trigger('click');
- expect(mrTabsMock.tabShown.mock.calls).toEqual([['pipelines']]);
- };
afterEach(() => {
wrapper.destroy();
- delete window.mrTabs;
});
- describe.each([false, true])(
- 'given the coreSecurityMrWidgetCounts feature flag is %p',
- (coreSecurityMrWidgetCounts) => {
- const createComponentWithFlag = (options) =>
- createComponent(
- merge(
- {
- provide: {
- glFeatures: {
- coreSecurityMrWidgetCounts,
- },
- },
- },
- options,
- ),
- );
-
- describe.each(SecurityReportsApp.reportTypes)('given a report type %p', (reportType) => {
- beforeEach(() => {
- window.mrTabs = { tabShown: jest.fn() };
- setupMockJobArtifact(reportType);
- createComponentWithFlag();
- return wrapper.vm.$nextTick();
- });
-
- it('calls the pipelineJobs API correctly', () => {
- expect(Api.pipelineJobs).toHaveBeenCalledTimes(1);
- expect(Api.pipelineJobs).toHaveBeenCalledWith(
- props.projectId,
- props.pipelineId,
- anyParams,
- );
- });
-
- it('renders the expected message', () => {
- expect(wrapper.text()).toMatchInterpolatedText(
- SecurityReportsApp.i18n.scansHaveRunWithDownloadGuidance,
- );
- });
-
- describe('clicking the anchor to the pipelines tab', () => {
- it('calls the mrTabs.tabShown global', () => {
- expectPipelinesTabAnchor();
- });
- });
-
- it('renders a help link', () => {
- expect(findHelpIconComponent().props()).toEqual({
- helpPath: props.securityReportsDocsPath,
- discoverProjectSecurityPath: props.discoverProjectSecurityPath,
- });
- });
+ describe('given the artifacts query is loading', () => {
+ beforeEach(() => {
+ createComponent({
+ apolloProvider: createMockApolloProvider(pendingHandler),
});
+ });
- describe('given a report type "foo"', () => {
- beforeEach(() => {
- setupMockJobArtifact('foo');
- createComponentWithFlag();
- return wrapper.vm.$nextTick();
- });
-
- it('calls the pipelineJobs API correctly', () => {
- expect(Api.pipelineJobs).toHaveBeenCalledTimes(1);
- expect(Api.pipelineJobs).toHaveBeenCalledWith(
- props.projectId,
- props.pipelineId,
- anyParams,
- );
- });
+ // TODO: Remove this assertion as part of
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/273431
+ it('initially renders nothing', () => {
+ expect(wrapper.html()).toBe('');
+ });
+ });
- it('renders nothing', () => {
- expect(wrapper.html()).toBe('');
- });
+ describe('given the artifacts query loads successfully', () => {
+ beforeEach(() => {
+ createComponent({
+ apolloProvider: createMockApolloProvider(successHandler),
});
+ });
- describe('security artifacts on last page of multi-page response', () => {
- const numPages = 3;
-
- beforeEach(() => {
- jest
- .spyOn(Api, 'pipelineJobs')
- .mockImplementation(async (projectId, pipelineId, { page }) => {
- const requestedPage = parseInt(page, 10);
- if (requestedPage < numPages) {
- return {
- // Some jobs with no relevant artifacts
- data: [{}, {}],
- headers: { 'x-next-page': String(requestedPage + 1) },
- };
- } else if (requestedPage === numPages) {
- return {
- data: [{ artifacts: [{ file_type: SecurityReportsApp.reportTypes[0] }] }],
- };
- }
-
- throw new Error('Test failed due to request of non-existent jobs page');
- });
-
- createComponentWithFlag();
- return wrapper.vm.$nextTick();
- });
+ it('renders the download dropdown', () => {
+ expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps);
+ });
- it('fetches all pages', () => {
- expect(Api.pipelineJobs).toHaveBeenCalledTimes(numPages);
- });
+ it('renders the expected message', () => {
+ expect(wrapper.text()).toContain(SecurityReportsApp.i18n.scansHaveRun);
+ });
- it('renders the expected message', () => {
- expect(wrapper.text()).toMatchInterpolatedText(
- SecurityReportsApp.i18n.scansHaveRunWithDownloadGuidance,
- );
- });
+ it('renders a help link', () => {
+ expect(findHelpIconComponent().props()).toEqual({
+ helpPath: props.securityReportsDocsPath,
+ discoverProjectSecurityPath: props.discoverProjectSecurityPath,
});
+ });
+ });
- describe('given an error from the API', () => {
- let error;
-
- beforeEach(() => {
- error = new Error('an error');
- jest.spyOn(Api, 'pipelineJobs').mockRejectedValue(error);
- createComponentWithFlag();
- return wrapper.vm.$nextTick();
- });
+ describe('given the artifacts query loads successfully with no artifacts', () => {
+ beforeEach(() => {
+ createComponent({
+ apolloProvider: createMockApolloProvider(successEmptyHandler),
+ });
+ });
- it('calls the pipelineJobs API correctly', () => {
- expect(Api.pipelineJobs).toHaveBeenCalledTimes(1);
- expect(Api.pipelineJobs).toHaveBeenCalledWith(
- props.projectId,
- props.pipelineId,
- anyParams,
- );
- });
+ // TODO: Remove this assertion as part of
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/273431
+ it('initially renders nothing', () => {
+ expect(wrapper.html()).toBe('');
+ });
+ });
- it('renders nothing', () => {
- expect(wrapper.html()).toBe('');
- });
+ describe('given the artifacts query fails', () => {
+ beforeEach(() => {
+ createComponent({
+ apolloProvider: createMockApolloProvider(failureHandler),
+ });
+ });
- it('calls createFlash correctly', () => {
- expect(createFlash.mock.calls).toEqual([
- [
- {
- message: SecurityReportsApp.i18n.apiError,
- captureError: true,
- error,
- },
- ],
- ]);
- });
+ it('calls createFlash correctly', () => {
+ expect(createFlash).toHaveBeenCalledWith({
+ message: SecurityReportsApp.i18n.apiError,
+ captureError: true,
+ error: expect.any(Error),
});
- },
- );
+ });
+
+ // TODO: Remove this assertion as part of
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/273431
+ it('renders nothing', () => {
+ expect(wrapper.html()).toBe('');
+ });
+ });
describe('given the coreSecurityMrWidgetCounts feature flag is enabled', () => {
let mock;
@@ -253,6 +160,7 @@ describe('Security reports app', () => {
coreSecurityMrWidgetCounts: true,
},
},
+ apolloProvider: createMockApolloProvider(successHandler),
}),
);
@@ -274,11 +182,7 @@ describe('Security reports app', () => {
${REPORT_TYPE_SECRET_DETECTION} | ${'secretScanningComparisonPath'} | ${SECRET_SCANNING_COMPARISON_PATH} | ${secretScanningDiffSuccessMock} | ${SECRET_SCANNING_SUCCESS_MESSAGE}
`(
'given a $pathProp and $reportType artifact',
- ({ reportType, pathProp, path, successResponse, successMessage }) => {
- beforeEach(() => {
- setupMockJobArtifact(reportType);
- });
-
+ ({ pathProp, path, successResponse, successMessage }) => {
describe('when loading', () => {
beforeEach(() => {
mock = new MockAdapter(axios, { delayResponse: 1 });
@@ -294,11 +198,11 @@ describe('Security reports app', () => {
});
it('should have loading message', () => {
- expect(wrapper.text()).toBe('Security scanning is loading');
+ expect(wrapper.text()).toContain('Security scanning is loading');
});
- it('should not render the pipeline tab anchor', () => {
- expect(findPipelinesTabAnchor().exists()).toBe(false);
+ it('renders the download dropdown', () => {
+ expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps);
});
});
@@ -319,8 +223,8 @@ describe('Security reports app', () => {
expect(trimText(wrapper.text())).toContain(successMessage);
});
- it('should render the pipeline tab anchor', () => {
- expectPipelinesTabAnchor();
+ it('renders the download dropdown', () => {
+ expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps);
});
});
@@ -341,125 +245,25 @@ describe('Security reports app', () => {
expect(trimText(wrapper.text())).toContain('Loading resulted in an error');
});
- it('should render the pipeline tab anchor', () => {
- expectPipelinesTabAnchor();
+ it('renders the download dropdown', () => {
+ expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps);
});
});
- },
- );
- });
-
- describe('given coreSecurityMrWidgetDownloads feature flag is enabled', () => {
- const createComponentWithFlagEnabled = (options) =>
- createComponent(
- merge(options, {
- provide: {
- glFeatures: {
- coreSecurityMrWidgetDownloads: true,
- },
- },
- }),
- );
-
- describe('given the query is loading', () => {
- beforeEach(() => {
- createComponentWithFlagEnabled({
- apolloProvider: createMockApolloProvider(pendingHandler),
- });
- });
- // TODO: Remove this assertion as part of
- // https://gitlab.com/gitlab-org/gitlab/-/issues/273431
- it('initially renders nothing', () => {
- expect(wrapper.html()).toBe('');
- });
- });
-
- describe('given the query loads successfully', () => {
- beforeEach(() => {
- createComponentWithFlagEnabled({
- apolloProvider: createMockApolloProvider(successHandler),
- });
- });
-
- it('renders the download dropdown', () => {
- expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps);
- });
-
- it('renders the expected message', () => {
- const text = wrapper.text();
- expect(text).not.toContain(SecurityReportsApp.i18n.scansHaveRunWithDownloadGuidance);
- expect(text).toContain(SecurityReportsApp.i18n.scansHaveRun);
- });
+ describe('when the comparison endpoint is not provided', () => {
+ beforeEach(() => {
+ mock.onGet(path).replyOnce(500);
- it('should not render the pipeline tab anchor', () => {
- expect(findPipelinesTabAnchor().exists()).toBe(false);
- });
- });
+ createComponentWithFlagEnabled();
- describe('given the query fails', () => {
- beforeEach(() => {
- createComponentWithFlagEnabled({
- apolloProvider: createMockApolloProvider(failureHandler),
- });
- });
+ return waitForPromises();
+ });
- it('calls createFlash correctly', () => {
- expect(createFlash).toHaveBeenCalledWith({
- message: SecurityReportsApp.i18n.apiError,
- captureError: true,
- error: expect.any(Error),
+ it('renders the basic scansHaveRun message', () => {
+ expect(wrapper.text()).toContain(SecurityReportsApp.i18n.scansHaveRun);
+ });
});
- });
-
- // TODO: Remove this assertion as part of
- // https://gitlab.com/gitlab-org/gitlab/-/issues/273431
- it('renders nothing', () => {
- expect(wrapper.html()).toBe('');
- });
- });
- });
-
- describe('given coreSecurityMrWidgetCounts and coreSecurityMrWidgetDownloads feature flags are enabled', () => {
- let mock;
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- mock.onGet(SAST_COMPARISON_PATH).replyOnce(200, sastDiffSuccessMock);
- mock.onGet(SECRET_SCANNING_COMPARISON_PATH).replyOnce(200, secretScanningDiffSuccessMock);
- createComponent({
- propsData: {
- sastComparisonPath: SAST_COMPARISON_PATH,
- secretScanningComparisonPath: SECRET_SCANNING_COMPARISON_PATH,
- },
- provide: {
- glFeatures: {
- coreSecurityMrWidgetCounts: true,
- coreSecurityMrWidgetDownloads: true,
- },
- },
- apolloProvider: createMockApolloProvider(successHandler),
- });
-
- return waitForPromises();
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- it('renders the download dropdown', () => {
- expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps);
- });
-
- it('renders the expected counts message', () => {
- expect(trimText(wrapper.text())).toContain(
- 'Security scanning detected 3 potential vulnerabilities 2 Critical 1 High and 0 Others',
- );
- });
-
- it('should not render the pipeline tab anchor', () => {
- expect(findPipelinesTabAnchor().exists()).toBe(false);
- });
+ },
+ );
});
});
diff --git a/spec/frontend/vue_shared/security_reports/store/getters_spec.js b/spec/frontend/vue_shared/security_reports/store/getters_spec.js
index b146a281d7b..97746c7c38b 100644
--- a/spec/frontend/vue_shared/security_reports/store/getters_spec.js
+++ b/spec/frontend/vue_shared/security_reports/store/getters_spec.js
@@ -1,7 +1,3 @@
-import createState from '~/vue_shared/security_reports/store/state';
-import createSastState from '~/vue_shared/security_reports/store/modules/sast/state';
-import createSecretScanningState from '~/vue_shared/security_reports/store/modules/secret_detection/state';
-import { groupedTextBuilder } from '~/vue_shared/security_reports/store/utils';
import {
groupedSummaryText,
allReportsHaveError,
@@ -11,6 +7,10 @@ import {
anyReportHasIssues,
summaryCounts,
} from '~/vue_shared/security_reports/store/getters';
+import createSastState from '~/vue_shared/security_reports/store/modules/sast/state';
+import createSecretScanningState from '~/vue_shared/security_reports/store/modules/secret_detection/state';
+import createState from '~/vue_shared/security_reports/store/state';
+import { groupedTextBuilder } from '~/vue_shared/security_reports/store/utils';
import { CRITICAL, HIGH, LOW } from '~/vulnerabilities/constants';
const generateVuln = (severity) => ({ severity });
diff --git a/spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js b/spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js
index c9d1db8a504..6af07273cf6 100644
--- a/spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js
+++ b/spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js
@@ -1,10 +1,10 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import createState from '~/vue_shared/security_reports/store/modules/sast/state';
-import * as types from '~/vue_shared/security_reports/store/modules/sast/mutation_types';
-import * as actions from '~/vue_shared/security_reports/store/modules/sast/actions';
import axios from '~/lib/utils/axios_utils';
+import * as actions from '~/vue_shared/security_reports/store/modules/sast/actions';
+import * as types from '~/vue_shared/security_reports/store/modules/sast/mutation_types';
+import createState from '~/vue_shared/security_reports/store/modules/sast/state';
const diffEndpoint = 'diff-endpoint.json';
const blobPath = 'blob-path.json';
diff --git a/spec/frontend/vue_shared/security_reports/store/modules/sast/mutations_spec.js b/spec/frontend/vue_shared/security_reports/store/modules/sast/mutations_spec.js
index fd611f38a34..d6119f44619 100644
--- a/spec/frontend/vue_shared/security_reports/store/modules/sast/mutations_spec.js
+++ b/spec/frontend/vue_shared/security_reports/store/modules/sast/mutations_spec.js
@@ -1,6 +1,6 @@
import * as types from '~/vue_shared/security_reports/store/modules/sast/mutation_types';
-import createState from '~/vue_shared/security_reports/store/modules/sast/state';
import mutations from '~/vue_shared/security_reports/store/modules/sast/mutations';
+import createState from '~/vue_shared/security_reports/store/modules/sast/state';
const createIssue = ({ ...config }) => ({ changed: false, ...config });
diff --git a/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js b/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js
index beda1a55438..d22fee864e7 100644
--- a/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js
+++ b/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js
@@ -1,10 +1,10 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import createState from '~/vue_shared/security_reports/store/modules/secret_detection/state';
-import * as types from '~/vue_shared/security_reports/store/modules/secret_detection/mutation_types';
-import * as actions from '~/vue_shared/security_reports/store/modules/secret_detection/actions';
import axios from '~/lib/utils/axios_utils';
+import * as actions from '~/vue_shared/security_reports/store/modules/secret_detection/actions';
+import * as types from '~/vue_shared/security_reports/store/modules/secret_detection/mutation_types';
+import createState from '~/vue_shared/security_reports/store/modules/secret_detection/state';
const diffEndpoint = 'diff-endpoint.json';
const blobPath = 'blob-path.json';
diff --git a/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/mutations_spec.js b/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/mutations_spec.js
index 13fcc0f47a3..42da7476a40 100644
--- a/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/mutations_spec.js
+++ b/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/mutations_spec.js
@@ -1,6 +1,6 @@
import * as types from '~/vue_shared/security_reports/store/modules/secret_detection/mutation_types';
-import createState from '~/vue_shared/security_reports/store/modules/secret_detection/state';
import mutations from '~/vue_shared/security_reports/store/modules/secret_detection/mutations';
+import createState from '~/vue_shared/security_reports/store/modules/secret_detection/state';
const createIssue = ({ ...config }) => ({ changed: false, ...config });
diff --git a/spec/frontend/vue_shared/security_reports/utils_spec.js b/spec/frontend/vue_shared/security_reports/utils_spec.js
index 7e5a27694ef..aa9e54fa10c 100644
--- a/spec/frontend/vue_shared/security_reports/utils_spec.js
+++ b/spec/frontend/vue_shared/security_reports/utils_spec.js
@@ -1,9 +1,9 @@
-import { extractSecurityReportArtifacts } from '~/vue_shared/security_reports/utils';
import {
REPORT_TYPE_SAST,
REPORT_TYPE_SECRET_DETECTION,
REPORT_FILE_TYPES,
} from '~/vue_shared/security_reports/constants';
+import { extractSecurityReportArtifacts } from '~/vue_shared/security_reports/utils';
import {
securityReportDownloadPathsQueryResponse,
sastArtifacts,
diff --git a/spec/frontend/vuex_shared/modules/modal/actions_spec.js b/spec/frontend/vuex_shared/modules/modal/actions_spec.js
index a8269194c0b..c151049df2d 100644
--- a/spec/frontend/vuex_shared/modules/modal/actions_spec.js
+++ b/spec/frontend/vuex_shared/modules/modal/actions_spec.js
@@ -1,6 +1,6 @@
import testAction from 'helpers/vuex_action_helper';
-import * as types from '~/vuex_shared/modules/modal/mutation_types';
import * as actions from '~/vuex_shared/modules/modal/actions';
+import * as types from '~/vuex_shared/modules/modal/mutation_types';
describe('Vuex ModalModule actions', () => {
describe('open', () => {
diff --git a/spec/frontend/vuex_shared/modules/modal/mutations_spec.js b/spec/frontend/vuex_shared/modules/modal/mutations_spec.js
index eaaf196d1ec..8e9ab6e6c86 100644
--- a/spec/frontend/vuex_shared/modules/modal/mutations_spec.js
+++ b/spec/frontend/vuex_shared/modules/modal/mutations_spec.js
@@ -1,5 +1,5 @@
-import mutations from '~/vuex_shared/modules/modal/mutations';
import * as types from '~/vuex_shared/modules/modal/mutation_types';
+import mutations from '~/vuex_shared/modules/modal/mutations';
describe('Vuex ModalModule mutations', () => {
describe(`${types.SHOW}`, () => {
diff --git a/spec/frontend/whats_new/components/app_spec.js b/spec/frontend/whats_new/components/app_spec.js
index 13e6bec47ab..ad062d04140 100644
--- a/spec/frontend/whats_new/components/app_spec.js
+++ b/spec/frontend/whats_new/components/app_spec.js
@@ -1,6 +1,6 @@
+import { GlDrawer, GlInfiniteScroll, GlTabs } from '@gitlab/ui';
import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex';
-import { GlDrawer, GlInfiniteScroll, GlTabs } from '@gitlab/ui';
import { mockTracking, unmockTracking, triggerEvent } from 'helpers/tracking_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import App from '~/whats_new/components/app.vue';
diff --git a/spec/frontend/whats_new/store/actions_spec.js b/spec/frontend/whats_new/store/actions_spec.js
index 82f17a2726f..c4125d28aba 100644
--- a/spec/frontend/whats_new/store/actions_spec.js
+++ b/spec/frontend/whats_new/store/actions_spec.js
@@ -1,10 +1,10 @@
-import testAction from 'helpers/vuex_action_helper';
-import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import MockAdapter from 'axios-mock-adapter';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
+import testAction from 'helpers/vuex_action_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import axios from '~/lib/utils/axios_utils';
import actions from '~/whats_new/store/actions';
import * as types from '~/whats_new/store/mutation_types';
-import axios from '~/lib/utils/axios_utils';
describe('whats new actions', () => {
describe('openDrawer', () => {
diff --git a/spec/frontend/whats_new/store/mutations_spec.js b/spec/frontend/whats_new/store/mutations_spec.js
index 4967fb51d2b..a95c6885a77 100644
--- a/spec/frontend/whats_new/store/mutations_spec.js
+++ b/spec/frontend/whats_new/store/mutations_spec.js
@@ -1,6 +1,6 @@
+import * as types from '~/whats_new/store/mutation_types';
import mutations from '~/whats_new/store/mutations';
import createState from '~/whats_new/store/state';
-import * as types from '~/whats_new/store/mutation_types';
describe('whats new mutations', () => {
let state;
diff --git a/spec/frontend/whats_new/utils/get_drawer_body_height_spec.js b/spec/frontend/whats_new/utils/get_drawer_body_height_spec.js
index d096a3cbdc6..b199f4f0c49 100644
--- a/spec/frontend/whats_new/utils/get_drawer_body_height_spec.js
+++ b/spec/frontend/whats_new/utils/get_drawer_body_height_spec.js
@@ -1,5 +1,5 @@
-import { mount } from '@vue/test-utils';
import { GlDrawer } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import { getDrawerBodyHeight } from '~/whats_new/utils/get_drawer_body_height';
describe('~/whats_new/utils/get_drawer_body_height', () => {
diff --git a/spec/frontend/zen_mode_spec.js b/spec/frontend/zen_mode_spec.js
index e874d018e92..5cc1d2200d3 100644
--- a/spec/frontend/zen_mode_spec.js
+++ b/spec/frontend/zen_mode_spec.js
@@ -1,11 +1,11 @@
-import $ from 'jquery';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import Dropzone from 'dropzone';
+import $ from 'jquery';
import Mousetrap from 'mousetrap';
+import initNotes from '~/init_notes';
import * as utils from '~/lib/utils/common_utils';
import ZenMode from '~/zen_mode';
-import initNotes from '~/init_notes';
describe('ZenMode', () => {
let mock;
diff --git a/spec/frontend_integration/ide/helpers/ide_helper.js b/spec/frontend_integration/ide/helpers/ide_helper.js
index 8d5d047b146..9e6bafc1297 100644
--- a/spec/frontend_integration/ide/helpers/ide_helper.js
+++ b/spec/frontend_integration/ide/helpers/ide_helper.js
@@ -69,7 +69,7 @@ const openFileRow = (row) => {
row.click();
};
-const findAndTraverseToPath = async (path, index = 0, row = null) => {
+export const findAndTraverseToPath = async (path, index = 0, row = null) => {
if (!path) {
return row;
}
@@ -110,6 +110,12 @@ const findAndClickRootAction = async (name) => {
button.click();
};
+/**
+ * Drop leading "/-/ide" and file path from the current URL
+ */
+export const getBaseRoute = (url = window.location.pathname) =>
+ url.replace(/^\/-\/ide/, '').replace(/\/-\/.*$/, '');
+
export const clickPreviewMarkdown = () => {
screen.getByText('Preview Markdown').click();
};
diff --git a/spec/frontend_integration/ide/helpers/start.js b/spec/frontend_integration/ide/helpers/start.js
index 43a996286e7..173a9610c84 100644
--- a/spec/frontend_integration/ide/helpers/start.js
+++ b/spec/frontend_integration/ide/helpers/start.js
@@ -1,14 +1,15 @@
import { TEST_HOST } from 'helpers/test_constants';
-import extendStore from '~/ide/stores/extend';
-import { IDE_DATASET } from './mock_data';
import { initIde } from '~/ide';
import Editor from '~/ide/lib/editor';
+import extendStore from '~/ide/stores/extend';
+import { IDE_DATASET } from './mock_data';
+
+export default (container, { isRepoEmpty = false, path = '', mrId = '' } = {}) => {
+ const projectName = isRepoEmpty ? 'lorem-ipsum-empty' : 'lorem-ipsum';
+ const pathSuffix = mrId ? `merge_requests/${mrId}` : `tree/master/-/${path}`;
-export default (container, { isRepoEmpty = false, path = '' } = {}) => {
global.jsdom.reconfigure({
- url: `${TEST_HOST}/-/ide/project/gitlab-test/lorem-ipsum${
- isRepoEmpty ? '-empty' : ''
- }/tree/master/-/${path}`,
+ url: `${TEST_HOST}/-/ide/project/gitlab-test/${projectName}/${pathSuffix}`,
});
const el = document.createElement('div');
diff --git a/spec/frontend_integration/ide/ide_integration_spec.js b/spec/frontend_integration/ide/ide_integration_spec.js
index 00a73661d14..3ce88de11fe 100644
--- a/spec/frontend_integration/ide/ide_integration_spec.js
+++ b/spec/frontend_integration/ide/ide_integration_spec.js
@@ -1,8 +1,8 @@
-import { waitForText } from 'helpers/wait_for_text';
-import waitForPromises from 'helpers/wait_for_promises';
import { setTestTimeout } from 'helpers/timeout';
-import { useOverclockTimers } from 'test_helpers/utils/overclock_timers';
+import waitForPromises from 'helpers/wait_for_promises';
+import { waitForText } from 'helpers/wait_for_text';
import { createCommitId } from 'test_helpers/factories/commit_id';
+import { useOverclockTimers } from 'test_helpers/utils/overclock_timers';
import * as ideHelper from './helpers/ide_helper';
import startWebIDE from './helpers/start';
diff --git a/spec/frontend_integration/ide/user_opens_file_spec.js b/spec/frontend_integration/ide/user_opens_file_spec.js
index 7fa6dcecc9e..2cb3363ef85 100644
--- a/spec/frontend_integration/ide/user_opens_file_spec.js
+++ b/spec/frontend_integration/ide/user_opens_file_spec.js
@@ -1,5 +1,5 @@
-import { useOverclockTimers } from 'test_helpers/utils/overclock_timers';
import { screen } from '@testing-library/dom';
+import { useOverclockTimers } from 'test_helpers/utils/overclock_timers';
import * as ideHelper from './helpers/ide_helper';
import startWebIDE from './helpers/start';
diff --git a/spec/frontend_integration/ide/user_opens_ide_spec.js b/spec/frontend_integration/ide/user_opens_ide_spec.js
index 502cb2e2c7d..f56cd008d1c 100644
--- a/spec/frontend_integration/ide/user_opens_ide_spec.js
+++ b/spec/frontend_integration/ide/user_opens_ide_spec.js
@@ -1,5 +1,5 @@
-import { useOverclockTimers } from 'test_helpers/utils/overclock_timers';
import { screen } from '@testing-library/dom';
+import { useOverclockTimers } from 'test_helpers/utils/overclock_timers';
import * as ideHelper from './helpers/ide_helper';
import startWebIDE from './helpers/start';
diff --git a/spec/frontend_integration/ide/user_opens_mr_spec.js b/spec/frontend_integration/ide/user_opens_mr_spec.js
new file mode 100644
index 00000000000..9cf0ff5da56
--- /dev/null
+++ b/spec/frontend_integration/ide/user_opens_mr_spec.js
@@ -0,0 +1,60 @@
+import { basename } from 'path';
+import { getMergeRequests, getMergeRequestWithChanges } from 'test_helpers/fixtures';
+import { useOverclockTimers } from 'test_helpers/utils/overclock_timers';
+import * as ideHelper from './helpers/ide_helper';
+import startWebIDE from './helpers/start';
+
+const getRelevantChanges = () =>
+ getMergeRequestWithChanges().changes.filter((x) => !x.deleted_file);
+
+describe('IDE: User opens Merge Request', () => {
+ useOverclockTimers();
+
+ let vm;
+ let container;
+ let changes;
+
+ beforeEach(async () => {
+ const [{ iid: mrId }] = getMergeRequests();
+
+ changes = getRelevantChanges();
+
+ setFixtures('<div class="webide-container"></div>');
+ container = document.querySelector('.webide-container');
+
+ vm = startWebIDE(container, { mrId });
+
+ await ideHelper.waitForTabToOpen(basename(changes[0].new_path));
+ await ideHelper.waitForMonacoEditor();
+ });
+
+ afterEach(async () => {
+ vm.$destroy();
+ vm = null;
+ });
+
+ const findAllTabs = () => Array.from(document.querySelectorAll('.multi-file-tab'));
+ const findAllTabsData = () =>
+ findAllTabs().map((el) => ({
+ title: el.getAttribute('title'),
+ text: el.textContent.trim(),
+ }));
+
+ it('shows first change as active in file tree', async () => {
+ const firstPath = changes[0].new_path;
+ const row = await ideHelper.findAndTraverseToPath(firstPath);
+
+ expect(row).toHaveClass('is-open');
+ expect(row).toHaveClass('is-active');
+ });
+
+ it('opens other changes', () => {
+ // We only show first 10 changes
+ const expectedTabs = changes.slice(0, 10).map((x) => ({
+ title: `${ideHelper.getBaseRoute()}/-/${x.new_path}/`,
+ text: basename(x.new_path),
+ }));
+
+ expect(findAllTabsData()).toEqual(expectedTabs);
+ });
+});
diff --git a/spec/frontend_integration/test_helpers/factories/commit.js b/spec/frontend_integration/test_helpers/factories/commit.js
index 09bb5fd589b..196295addbe 100644
--- a/spec/frontend_integration/test_helpers/factories/commit.js
+++ b/spec/frontend_integration/test_helpers/factories/commit.js
@@ -1,5 +1,5 @@
-import { withValues } from '../utils/obj';
import { getCommit } from '../fixtures';
+import { withValues } from '../utils/obj';
import { createCommitId } from './commit_id';
export const createNewCommit = ({ id = createCommitId(), message }, orig = getCommit()) => {
diff --git a/spec/frontend_integration/test_helpers/fixtures.js b/spec/frontend_integration/test_helpers/fixtures.js
index fde3fd8cb63..b2768440607 100644
--- a/spec/frontend_integration/test_helpers/fixtures.js
+++ b/spec/frontend_integration/test_helpers/fixtures.js
@@ -1,4 +1,10 @@
-/* eslint-disable global-require, import/no-unresolved */
+/* eslint-disable global-require */
+// We use "require" rather than `fs` so that this works in a browser environment.
+
+/* eslint "import/no-unresolved": 0 */
+// We don't want to require *all* fixtures to be generated (especailly in a local environment).
+// We use `eslint` instead of `eslint-disable`, so that we also don't trigger an `Unused eslint-disable directive` when all fixtures are present.
+
import { memoize } from 'lodash';
const createFactoryWithDefault = (fn, defaultValue) => () => {
@@ -25,6 +31,12 @@ export const getBranch = factory.json(() =>
export const getMergeRequests = factory.json(() =>
require('test_fixtures/api/merge_requests/get.json'),
);
+export const getMergeRequestWithChanges = factory.json(() =>
+ require('test_fixtures/api/merge_requests/changes.json'),
+);
+export const getMergeRequestVersions = factory.json(() =>
+ require('test_fixtures/api/merge_requests/versions.json'),
+);
export const getRepositoryFiles = factory.json(() =>
require('test_fixtures/projects_json/files.json'),
);
diff --git a/spec/frontend_integration/test_helpers/mock_server/index.js b/spec/frontend_integration/test_helpers/mock_server/index.js
index 2aebdefaafb..20cb441daa7 100644
--- a/spec/frontend_integration/test_helpers/mock_server/index.js
+++ b/spec/frontend_integration/test_helpers/mock_server/index.js
@@ -4,6 +4,8 @@ import {
getEmptyProject,
getBranch,
getMergeRequests,
+ getMergeRequestWithChanges,
+ getMergeRequestVersions,
getRepositoryFiles,
getBlobReadme,
getBlobImage,
@@ -16,6 +18,8 @@ export const createMockServerOptions = () => ({
project: Model,
branch: Model,
mergeRequest: Model,
+ mergeRequestChange: Model,
+ mergeRequestVersion: Model,
file: Model,
userPermission: Model,
},
@@ -30,6 +34,8 @@ export const createMockServerOptions = () => ({
projects: [getProject(), getEmptyProject()],
branches: [getBranch()],
mergeRequests: getMergeRequests(),
+ mergeRequestChanges: [getMergeRequestWithChanges()],
+ mergeRequestVersions: getMergeRequestVersions(),
filesRaw: [
{
raw: getBlobReadme(),
diff --git a/spec/frontend_integration/test_helpers/mock_server/routes/404.js b/spec/frontend_integration/test_helpers/mock_server/routes/404.js
index bc8edba927e..54183f1189c 100644
--- a/spec/frontend_integration/test_helpers/mock_server/routes/404.js
+++ b/spec/frontend_integration/test_helpers/mock_server/routes/404.js
@@ -1,3 +1,5 @@
+import { Response } from 'miragejs';
+
export default (server) => {
['get', 'post', 'put', 'delete', 'patch'].forEach((method) => {
server[method]('*', () => {
diff --git a/spec/frontend_integration/test_helpers/mock_server/routes/projects.js b/spec/frontend_integration/test_helpers/mock_server/routes/projects.js
index de37aa98eee..e6e09121fd4 100644
--- a/spec/frontend_integration/test_helpers/mock_server/routes/projects.js
+++ b/spec/frontend_integration/test_helpers/mock_server/routes/projects.js
@@ -20,4 +20,22 @@ export default (server) => {
return result.models;
});
+
+ server.get('/api/v4/projects/:id/merge_requests/:mid', (schema, request) => {
+ const mr = schema.mergeRequests.findBy({ iid: request.params.mid });
+
+ return mr.attrs;
+ });
+
+ server.get('/api/v4/projects/:id/merge_requests/:mid/versions', (schema, request) => {
+ const versions = schema.mergeRequestVersions.where({ merge_request_id: request.params.mid });
+
+ return versions.models;
+ });
+
+ server.get('/api/v4/projects/:id/merge_requests/:mid/changes', (schema, request) => {
+ const mrWithChanges = schema.mergeRequestChanges.findBy({ iid: request.params.mid });
+
+ return mrWithChanges.attrs;
+ });
};
diff --git a/spec/graphql/mutations/boards/lists/create_spec.rb b/spec/graphql/mutations/boards/lists/create_spec.rb
index 894dd1f34b4..815064e7c58 100644
--- a/spec/graphql/mutations/boards/lists/create_spec.rb
+++ b/spec/graphql/mutations/boards/lists/create_spec.rb
@@ -3,84 +3,8 @@
require 'spec_helper'
RSpec.describe Mutations::Boards::Lists::Create do
- include GraphqlHelpers
-
let_it_be(:group) { create(:group, :private) }
let_it_be(:board) { create(:board, group: group) }
- let_it_be(:user) { create(:user) }
- let_it_be(:guest) { create(:user) }
-
- let(:current_user) { user }
- let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) }
- let(:list_create_params) { {} }
-
- before_all do
- group.add_reporter(user)
- group.add_guest(guest)
- end
-
- subject { mutation.resolve(board_id: board.to_global_id.to_s, **list_create_params) }
-
- describe '#ready?' do
- it 'raises an error if required arguments are missing' do
- expect { mutation.ready?(board_id: 'some id') }
- .to raise_error(Gitlab::Graphql::Errors::ArgumentError, /one and only one of/)
- end
-
- it 'raises an error if too many required arguments are specified' do
- expect { mutation.ready?(board_id: 'some id', backlog: true, label_id: 'some label') }
- .to raise_error(Gitlab::Graphql::Errors::ArgumentError, /one and only one of/)
- end
- end
-
- describe '#resolve' do
- context 'with proper permissions' do
- describe 'backlog list' do
- let(:list_create_params) { { backlog: true } }
-
- it 'creates one and only one backlog' do
- expect { subject }.to change { board.lists.backlog.count }.from(0).to(1)
- expect(board.lists.backlog.first.list_type).to eq 'backlog'
-
- backlog_id = board.lists.backlog.first.id
-
- expect { subject }.not_to change { board.lists.backlog.count }
- expect(board.lists.backlog.last.id).to eq backlog_id
- end
- end
-
- describe 'label list' do
- let_it_be(:dev_label) do
- create(:group_label, title: 'Development', color: '#FFAABB', group: group)
- end
-
- let(:list_create_params) { { label_id: dev_label.to_global_id.to_s } }
-
- it 'creates a new issue board list for labels' do
- expect { subject }.to change { board.lists.count }.from(1).to(2)
-
- new_list = subject[:list]
-
- expect(new_list.title).to eq dev_label.title
- expect(new_list.position).to eq 0
- end
-
- context 'when label not found' do
- let(:list_create_params) { { label_id: "gid://gitlab/Label/#{non_existing_record_id}" } }
-
- it 'returns an error' do
- expect(subject[:errors]).to include 'Label not found'
- end
- end
- end
- end
-
- context 'without proper permissions' do
- let(:current_user) { guest }
- it 'raises an error' do
- expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
- end
- end
- end
+ it_behaves_like 'board lists create mutation'
end
diff --git a/spec/graphql/mutations/concerns/mutations/can_mutate_spammable_spec.rb b/spec/graphql/mutations/concerns/mutations/can_mutate_spammable_spec.rb
new file mode 100644
index 00000000000..ee8db7a1f31
--- /dev/null
+++ b/spec/graphql/mutations/concerns/mutations/can_mutate_spammable_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::CanMutateSpammable do
+ let(:mutation_class) do
+ Class.new(Mutations::BaseMutation) do
+ include Mutations::CanMutateSpammable
+ end
+ end
+
+ let(:request) { double(:request) }
+ let(:query) { double(:query, schema: GitlabSchema) }
+ let(:context) { GraphQL::Query::Context.new(query: query, object: nil, values: { request: request }) }
+
+ subject(:mutation) { mutation_class.new(object: nil, context: context, field: nil) }
+
+ describe '#additional_spam_params' do
+ it 'returns additional spam-related params' do
+ expect(subject.send(:additional_spam_params)).to eq({ api: true, request: request })
+ end
+ end
+
+ describe '#with_spam_action_fields' do
+ let(:spam_log) { double(:spam_log, id: 1) }
+ let(:spammable) { double(:spammable, spam?: true, render_recaptcha?: true, spam_log: spam_log) }
+
+ before do
+ allow(Gitlab::CurrentSettings).to receive(:recaptcha_site_key) { 'abc123' }
+ end
+
+ it 'merges in spam action fields from spammable' do
+ result = subject.send(:with_spam_action_fields, spammable) do
+ { other_field: true }
+ end
+ expect(result)
+ .to eq({
+ spam: true,
+ needs_captcha_response: true,
+ spam_log_id: 1,
+ captcha_site_key: 'abc123',
+ other_field: true
+ })
+ end
+ end
+end
diff --git a/spec/graphql/mutations/merge_requests/update_spec.rb b/spec/graphql/mutations/merge_requests/update_spec.rb
index 8acd2562ea8..206abaf34ce 100644
--- a/spec/graphql/mutations/merge_requests/update_spec.rb
+++ b/spec/graphql/mutations/merge_requests/update_spec.rb
@@ -12,10 +12,11 @@ RSpec.describe Mutations::MergeRequests::Update do
describe '#resolve' do
let(:attributes) { { title: 'new title', description: 'new description', target_branch: 'new-branch' } }
+ let(:arguments) { attributes }
let(:mutated_merge_request) { subject[:merge_request] }
subject do
- mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, **attributes)
+ mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, **arguments)
end
it_behaves_like 'permission level for merge request mutation is correctly verified'
@@ -61,6 +62,24 @@ RSpec.describe Mutations::MergeRequests::Update do
expect(mutated_merge_request).to have_attributes(attributes)
end
end
+
+ context 'when closing the MR' do
+ let(:arguments) { { state_event: ::Types::MergeRequestStateEventEnum.values['CLOSED'].value } }
+
+ it 'closes the MR' do
+ expect(mutated_merge_request).to be_closed
+ end
+ end
+
+ context 'when re-opening the MR' do
+ let(:arguments) { { state_event: ::Types::MergeRequestStateEventEnum.values['OPEN'].value } }
+
+ it 'closes the MR' do
+ merge_request.close!
+
+ expect(mutated_merge_request).to be_open
+ end
+ end
end
end
end
diff --git a/spec/graphql/mutations/security/ci_configuration/configure_sast_spec.rb b/spec/graphql/mutations/security/ci_configuration/configure_sast_spec.rb
new file mode 100644
index 00000000000..ed03a1cb906
--- /dev/null
+++ b/spec/graphql/mutations/security/ci_configuration/configure_sast_spec.rb
@@ -0,0 +1,120 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Security::CiConfiguration::ConfigureSast do
+ subject(:mutation) { described_class.new(object: nil, context: context, field: nil) }
+
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:user) { create(:user) }
+
+ let_it_be(:service_result_json) do
+ {
+ status: "success",
+ success_path: "http://127.0.0.1:3000/root/demo-historic-secrets/-/merge_requests/new?",
+ errors: nil
+ }
+ end
+
+ let_it_be(:service_error_result_json) do
+ {
+ status: "error",
+ success_path: nil,
+ errors: %w(error1 error2)
+ }
+ end
+
+ let(:context) do
+ GraphQL::Query::Context.new(
+ query: OpenStruct.new(schema: nil),
+ values: { current_user: user },
+ object: nil
+ )
+ end
+
+ specify { expect(described_class).to require_graphql_authorizations(:push_code) }
+
+ describe '#resolve' do
+ subject { mutation.resolve(project_path: project.full_path, configuration: {}) }
+
+ let(:result) { subject }
+
+ it 'raises an error if the resource is not accessible to the user' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+
+ context 'when user does not have enough permissions' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+
+ context 'when user is a maintainer of a different project' do
+ before do
+ create(:project_empty_repo).add_maintainer(user)
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+
+ context 'when the user does not have permission to create a new branch' do
+ before_all do
+ project.add_developer(user)
+ end
+
+ let(:error_message) { 'You are not allowed to create protected branches on this project.' }
+
+ it 'returns an array of errors' do
+ allow_next_instance_of(::Files::MultiService) do |multi_service|
+ allow(multi_service).to receive(:execute).and_raise(Gitlab::Git::PreReceiveError.new("GitLab: #{error_message}"))
+ end
+
+ expect(result).to match(
+ status: :error,
+ success_path: nil,
+ errors: match_array([error_message])
+ )
+ end
+ end
+
+ context 'when the user can create a merge request' do
+ before_all do
+ project.add_developer(user)
+ end
+
+ context 'when service successfully generates a path to create a new merge request' do
+ it 'returns a success path' do
+ allow_next_instance_of(::Security::CiConfiguration::SastCreateService) do |service|
+ allow(service).to receive(:execute).and_return(service_result_json)
+ end
+
+ expect(result).to match(
+ status: 'success',
+ success_path: service_result_json[:success_path],
+ errors: []
+ )
+ end
+ end
+
+ context 'when service can not generate any path to create a new merge request' do
+ it 'returns an array of errors' do
+ allow_next_instance_of(::Security::CiConfiguration::SastCreateService) do |service|
+ allow(service).to receive(:execute).and_return(service_error_result_json)
+ end
+
+ expect(result).to match(
+ status: 'error',
+ success_path: be_nil,
+ errors: match_array(service_error_result_json[:errors])
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/spec/graphql/resolvers/base_resolver_spec.rb b/spec/graphql/resolvers/base_resolver_spec.rb
index 8a24b69eb6f..8d2ae238bfe 100644
--- a/spec/graphql/resolvers/base_resolver_spec.rb
+++ b/spec/graphql/resolvers/base_resolver_spec.rb
@@ -277,8 +277,8 @@ RSpec.describe Resolvers::BaseResolver do
describe '#offset_pagination' do
let(:instance) { resolver_instance(resolver) }
- it 'is sugar for OffsetActiveRecordRelationConnection.new' do
- expect(instance.offset_pagination(User.none)).to be_a(::Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection)
+ it 'is sugar for OffsetPaginatedRelation.new' do
+ expect(instance.offset_pagination(User.none)).to be_a(::Gitlab::Graphql::Pagination::OffsetPaginatedRelation)
end
end
end
diff --git a/spec/graphql/resolvers/board_list_issues_resolver_spec.rb b/spec/graphql/resolvers/board_list_issues_resolver_spec.rb
index e7c56a526f4..5eda840854a 100644
--- a/spec/graphql/resolvers/board_list_issues_resolver_spec.rb
+++ b/spec/graphql/resolvers/board_list_issues_resolver_spec.rb
@@ -23,19 +23,19 @@ RSpec.describe Resolvers::BoardListIssuesResolver do
it 'returns the issues in the correct order' do
# by relative_position and then ID
- issues = resolve_board_list_issues.items
+ issues = resolve_board_list_issues
expect(issues.map(&:id)).to eq [issue3.id, issue1.id, issue2.id]
end
it 'finds only issues matching filters' do
- result = resolve_board_list_issues(args: { filters: { label_name: [label.title], not: { label_name: [label2.title] } } }).items
+ result = resolve_board_list_issues(args: { filters: { label_name: [label.title], not: { label_name: [label2.title] } } })
expect(result).to match_array([issue1, issue3])
end
it 'finds only issues matching search param' do
- result = resolve_board_list_issues(args: { filters: { search: issue1.title } }).items
+ result = resolve_board_list_issues(args: { filters: { search: issue1.title } })
expect(result).to match_array([issue1])
end
diff --git a/spec/graphql/resolvers/board_lists_resolver_spec.rb b/spec/graphql/resolvers/board_lists_resolver_spec.rb
index 71ebec4dc7e..fdcebd30bb3 100644
--- a/spec/graphql/resolvers/board_lists_resolver_spec.rb
+++ b/spec/graphql/resolvers/board_lists_resolver_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe Resolvers::BoardListsResolver do
end
it 'does not create the backlog list' do
- lists = resolve_board_lists.items
+ lists = resolve_board_lists
expect(lists.count).to eq 1
expect(lists[0].list_type).to eq 'closed'
@@ -38,7 +38,7 @@ RSpec.describe Resolvers::BoardListsResolver do
let!(:backlog_list) { create(:backlog_list, board: board) }
it 'returns a list of board lists' do
- lists = resolve_board_lists.items
+ lists = resolve_board_lists
expect(lists.count).to eq 3
expect(lists.map(&:list_type)).to eq %w(backlog label closed)
@@ -50,7 +50,7 @@ RSpec.describe Resolvers::BoardListsResolver do
end
it 'returns the complete list of board lists for this user' do
- lists = resolve_board_lists.items
+ lists = resolve_board_lists
expect(lists.count).to eq 3
end
@@ -58,7 +58,7 @@ RSpec.describe Resolvers::BoardListsResolver do
context 'when querying for a single list' do
it 'returns specified list' do
- list = resolve_board_lists(args: { id: global_id_of(label_list) }).items
+ list = resolve_board_lists(args: { id: global_id_of(label_list) })
expect(list).to eq [label_list]
end
@@ -69,13 +69,13 @@ RSpec.describe Resolvers::BoardListsResolver do
external_label = create(:group_label, group: group)
external_list = create(:list, board: external_board, label: external_label)
- list = resolve_board_lists(args: { id: global_id_of(external_list) }).items
+ list = resolve_board_lists(args: { id: global_id_of(external_list) })
expect(list).to eq List.none
end
it 'raises an argument error if list ID is not valid' do
- expect { resolve_board_lists(args: { id: 'test' }).items }
+ expect { resolve_board_lists(args: { id: 'test' }) }
.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
end
end
diff --git a/spec/graphql/resolvers/ci/config_resolver_spec.rb b/spec/graphql/resolvers/ci/config_resolver_spec.rb
index ca7ae73fef8..73e9fab9f99 100644
--- a/spec/graphql/resolvers/ci/config_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/config_resolver_spec.rb
@@ -36,7 +36,8 @@ RSpec.describe Resolvers::Ci::ConfigResolver do
File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci_includes.yml'))
end
- it 'lints the ci config file' do
+ it 'lints the ci config file and returns the merged yaml file' do
+ expect(response[:merged_yaml]).to eq(content)
expect(response[:status]).to eq(:valid)
expect(response[:errors]).to be_empty
end
diff --git a/spec/graphql/resolvers/container_repositories_resolver_spec.rb b/spec/graphql/resolvers/container_repositories_resolver_spec.rb
index b888d79626e..a17d2a7b0d5 100644
--- a/spec/graphql/resolvers/container_repositories_resolver_spec.rb
+++ b/spec/graphql/resolvers/container_repositories_resolver_spec.rb
@@ -27,6 +27,34 @@ RSpec.describe Resolvers::ContainerRepositoriesResolver do
it { is_expected.to contain_exactly(named_container_repository) }
end
+
+ context 'with a sort argument' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:sort_repository) do
+ create(:container_repository, name: 'bar', project: project, created_at: 1.day.ago)
+ end
+
+ let_it_be(:sort_repository2) do
+ create(:container_repository, name: 'foo', project: project, created_at: 1.hour.ago, updated_at: 1.hour.ago)
+ end
+
+ [:created_desc, :updated_asc, :name_desc].each do |order|
+ context "#{order}" do
+ let(:args) { { sort: order } }
+
+ it { is_expected.to eq([sort_repository2, sort_repository]) }
+ end
+ end
+
+ [:created_asc, :updated_desc, :name_asc].each do |order|
+ context "#{order}" do
+ let(:args) { { sort: order } }
+
+ it { is_expected.to eq([sort_repository, sort_repository2]) }
+ end
+ end
+ end
end
context 'with authorized user' do
diff --git a/spec/graphql/resolvers/group_labels_resolver_spec.rb b/spec/graphql/resolvers/group_labels_resolver_spec.rb
new file mode 100644
index 00000000000..ed94f12502a
--- /dev/null
+++ b/spec/graphql/resolvers/group_labels_resolver_spec.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::GroupLabelsResolver do
+ include GraphqlHelpers
+
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:group, reload: true) { create(:group, :private) }
+ let_it_be(:subgroup, reload: true) { create(:group, :private, parent: group) }
+ let_it_be(:sub_subgroup, reload: true) { create(:group, :private, parent: subgroup) }
+ let_it_be(:project, reload: true) { create(:project, :private, group: sub_subgroup) }
+ let_it_be(:label1) { create(:label, project: project, name: 'project feature') }
+ let_it_be(:label2) { create(:label, project: project, name: 'new project feature') }
+ let_it_be(:group_label1) { create(:group_label, group: group, name: 'group feature') }
+ let_it_be(:group_label2) { create(:group_label, group: group, name: 'new group feature') }
+ let_it_be(:subgroup_label1) { create(:group_label, group: subgroup, name: 'subgroup feature') }
+ let_it_be(:subgroup_label2) { create(:group_label, group: subgroup, name: 'new subgroup feature') }
+ let_it_be(:sub_subgroup_label1) { create(:group_label, group: sub_subgroup, name: 'sub_subgroup feature') }
+ let_it_be(:sub_subgroup_label2) { create(:group_label, group: sub_subgroup, name: 'new sub_subgroup feature') }
+
+ specify do
+ expect(described_class).to have_nullable_graphql_type(Types::LabelType.connection_type)
+ end
+
+ describe '#resolve' do
+ context 'with unauthorized user' do
+ it 'raises error' do
+ expect { resolve_labels(subgroup) }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+
+ context 'with authorized user' do
+ it 'does not raise error' do
+ group.add_guest(current_user)
+
+ expect { resolve_labels(subgroup) }.not_to raise_error
+ end
+ end
+
+ context 'without parent' do
+ it 'returns no labels' do
+ expect(resolve_labels(nil)).to eq(Label.none)
+ end
+ end
+
+ context 'at group level' do
+ before_all do
+ group.add_developer(current_user)
+ end
+
+ # because :include_ancestor_groups, :include_descendant_groups, :only_group_labels default to false
+ # the `nil` value would be equivalent to passing in `false` so just check for `nil` option
+ where(:include_ancestor_groups, :include_descendant_groups, :only_group_labels, :search_term, :test) do
+ nil | nil | nil | nil | -> { expect(subject).to contain_exactly(subgroup_label1, subgroup_label2) }
+ nil | nil | true | nil | -> { expect(subject).to contain_exactly(subgroup_label1, subgroup_label2) }
+ nil | true | nil | nil | -> { expect(subject).to contain_exactly(subgroup_label1, subgroup_label2, sub_subgroup_label1, sub_subgroup_label2, label1, label2) }
+ nil | true | true | nil | -> { expect(subject).to contain_exactly(subgroup_label1, subgroup_label2, sub_subgroup_label1, sub_subgroup_label2) }
+ true | nil | nil | nil | -> { expect(subject).to contain_exactly(group_label1, group_label2, subgroup_label1, subgroup_label2) }
+ true | nil | true | nil | -> { expect(subject).to contain_exactly(group_label1, group_label2, subgroup_label1, subgroup_label2) }
+ true | true | nil | nil | -> { expect(subject).to contain_exactly(group_label1, group_label2, subgroup_label1, subgroup_label2, sub_subgroup_label1, sub_subgroup_label2, label1, label2) }
+ true | true | true | nil | -> { expect(subject).to contain_exactly(group_label1, group_label2, subgroup_label1, subgroup_label2, sub_subgroup_label1, sub_subgroup_label2) }
+
+ nil | nil | nil | 'new' | -> { expect(subject).to contain_exactly(subgroup_label2) }
+ nil | nil | true | 'new' | -> { expect(subject).to contain_exactly(subgroup_label2) }
+ nil | true | nil | 'new' | -> { expect(subject).to contain_exactly(subgroup_label2, sub_subgroup_label2, label2) }
+ nil | true | true | 'new' | -> { expect(subject).to contain_exactly(subgroup_label2, sub_subgroup_label2) }
+ true | nil | nil | 'new' | -> { expect(subject).to contain_exactly(group_label2, subgroup_label2) }
+ true | nil | true | 'new' | -> { expect(subject).to contain_exactly(group_label2, subgroup_label2) }
+ true | true | nil | 'new' | -> { expect(subject).to contain_exactly(group_label2, subgroup_label2, sub_subgroup_label2, label2) }
+ true | true | true | 'new' | -> { expect(subject).to contain_exactly(group_label2, subgroup_label2, sub_subgroup_label2) }
+ end
+
+ with_them do
+ let(:params) do
+ {
+ include_ancestor_groups: include_ancestor_groups,
+ include_descendant_groups: include_descendant_groups,
+ only_group_labels: only_group_labels,
+ search_term: search_term
+ }
+ end
+
+ subject { resolve_labels(subgroup, params) }
+
+ it { self.instance_exec(&test) }
+ end
+ end
+ end
+
+ def resolve_labels(parent, args = {}, context = { current_user: current_user })
+ resolve(described_class, obj: parent, args: args, ctx: context)
+ end
+end
diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb
index 269ee9eabf9..8980f4aa19d 100644
--- a/spec/graphql/resolvers/issues_resolver_spec.rb
+++ b/spec/graphql/resolvers/issues_resolver_spec.rb
@@ -195,11 +195,11 @@ RSpec.describe Resolvers::IssuesResolver do
let_it_be(:priority_issue4) { create(:issue, project: project) }
it 'sorts issues ascending' do
- expect(resolve_issues(sort: :priority_asc).items).to eq([priority_issue3, priority_issue1, priority_issue2, priority_issue4])
+ expect(resolve_issues(sort: :priority_asc).to_a).to eq([priority_issue3, priority_issue1, priority_issue2, priority_issue4])
end
it 'sorts issues descending' do
- expect(resolve_issues(sort: :priority_desc).items).to eq([priority_issue1, priority_issue3, priority_issue2, priority_issue4])
+ expect(resolve_issues(sort: :priority_desc).to_a).to eq([priority_issue1, priority_issue3, priority_issue2, priority_issue4])
end
end
@@ -214,11 +214,11 @@ RSpec.describe Resolvers::IssuesResolver do
let_it_be(:label_issue4) { create(:issue, project: project) }
it 'sorts issues ascending' do
- expect(resolve_issues(sort: :label_priority_asc).items).to eq([label_issue3, label_issue1, label_issue2, label_issue4])
+ expect(resolve_issues(sort: :label_priority_asc).to_a).to eq([label_issue3, label_issue1, label_issue2, label_issue4])
end
it 'sorts issues descending' do
- expect(resolve_issues(sort: :label_priority_desc).items).to eq([label_issue2, label_issue3, label_issue1, label_issue4])
+ expect(resolve_issues(sort: :label_priority_desc).to_a).to eq([label_issue2, label_issue3, label_issue1, label_issue4])
end
end
@@ -231,11 +231,11 @@ RSpec.describe Resolvers::IssuesResolver do
let_it_be(:milestone_issue3) { create(:issue, project: project, milestone: late_milestone) }
it 'sorts issues ascending' do
- expect(resolve_issues(sort: :milestone_due_asc).items).to eq([milestone_issue2, milestone_issue3, milestone_issue1])
+ expect(resolve_issues(sort: :milestone_due_asc).to_a).to eq([milestone_issue2, milestone_issue3, milestone_issue1])
end
it 'sorts issues descending' do
- expect(resolve_issues(sort: :milestone_due_desc).items).to eq([milestone_issue3, milestone_issue2, milestone_issue1])
+ expect(resolve_issues(sort: :milestone_due_desc).to_a).to eq([milestone_issue3, milestone_issue2, milestone_issue1])
end
end
diff --git a/spec/graphql/resolvers/labels_resolver_spec.rb b/spec/graphql/resolvers/labels_resolver_spec.rb
new file mode 100644
index 00000000000..3d027a6c8d5
--- /dev/null
+++ b/spec/graphql/resolvers/labels_resolver_spec.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::LabelsResolver do
+ include GraphqlHelpers
+
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:group, reload: true) { create(:group, :private) }
+ let_it_be(:subgroup, reload: true) { create(:group, :private, parent: group) }
+ let_it_be(:sub_subgroup, reload: true) { create(:group, :private, parent: subgroup) }
+ let_it_be(:project, reload: true) { create(:project, :private, group: subgroup) }
+ let_it_be(:label1) { create(:label, project: project, name: 'project feature') }
+ let_it_be(:label2) { create(:label, project: project, name: 'new project feature') }
+ let_it_be(:group_label1) { create(:group_label, group: group, name: 'group feature') }
+ let_it_be(:group_label2) { create(:group_label, group: group, name: 'new group feature') }
+ let_it_be(:subgroup_label1) { create(:group_label, group: subgroup, name: 'subgroup feature') }
+ let_it_be(:subgroup_label2) { create(:group_label, group: subgroup, name: 'new subgroup feature') }
+ let_it_be(:sub_subgroup_label1) { create(:group_label, group: sub_subgroup, name: 'sub_subgroup feature') }
+ let_it_be(:sub_subgroup_label2) { create(:group_label, group: sub_subgroup, name: 'new sub_subgroup feature') }
+
+ specify do
+ expect(described_class).to have_nullable_graphql_type(Types::LabelType.connection_type)
+ end
+
+ describe '#resolve' do
+ context 'with unauthorized user' do
+ it 'returns no labels' do
+ expect { resolve_labels(project) }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+
+ context 'with authorized user' do
+ it 'returns no labels' do
+ group.add_guest(current_user)
+
+ expect { resolve_labels(project) }.not_to raise_error
+ end
+ end
+
+ context 'without parent' do
+ it 'returns no labels' do
+ expect(resolve_labels(nil)).to eq(Label.none)
+ end
+ end
+
+ context 'at project level' do
+ before_all do
+ group.add_developer(current_user)
+ end
+
+ # because :include_ancestor_groups, :include_descendant_groups, :only_group_labels default to false
+ # the `nil` value would be equivalent to passing in `false` so just check for `nil` option
+ where(:include_ancestor_groups, :include_descendant_groups, :only_group_labels, :search_term, :test) do
+ nil | nil | nil | nil | -> { expect(subject).to contain_exactly(label1, label2, subgroup_label1, subgroup_label2) }
+ nil | nil | true | nil | -> { expect(subject).to contain_exactly(label1, label2, subgroup_label1, subgroup_label2) }
+ nil | true | nil | nil | -> { expect(subject).to contain_exactly(label1, label2, subgroup_label1, subgroup_label2, sub_subgroup_label1, sub_subgroup_label2) }
+ nil | true | true | nil | -> { expect(subject).to contain_exactly(label1, label2, subgroup_label1, subgroup_label2, sub_subgroup_label1, sub_subgroup_label2) }
+ true | nil | nil | nil | -> { expect(subject).to contain_exactly(label1, label2, group_label1, group_label2, subgroup_label1, subgroup_label2) }
+ true | nil | true | nil | -> { expect(subject).to contain_exactly(label1, label2, group_label1, group_label2, subgroup_label1, subgroup_label2) }
+ true | true | nil | nil | -> { expect(subject).to contain_exactly(label1, label2, group_label1, group_label2, subgroup_label1, subgroup_label2, sub_subgroup_label1, sub_subgroup_label2) }
+ true | true | true | nil | -> { expect(subject).to contain_exactly(label1, label2, group_label1, group_label2, subgroup_label1, subgroup_label2, sub_subgroup_label1, sub_subgroup_label2) }
+
+ nil | nil | nil | 'new' | -> { expect(subject).to contain_exactly(label2, subgroup_label2) }
+ nil | nil | true | 'new' | -> { expect(subject).to contain_exactly(label2, subgroup_label2) }
+ nil | true | nil | 'new' | -> { expect(subject).to contain_exactly(label2, subgroup_label2, sub_subgroup_label2) }
+ nil | true | true | 'new' | -> { expect(subject).to contain_exactly(label2, subgroup_label2, sub_subgroup_label2) }
+ true | nil | nil | 'new' | -> { expect(subject).to contain_exactly(label2, group_label2, subgroup_label2) }
+ true | nil | true | 'new' | -> { expect(subject).to contain_exactly(label2, group_label2, subgroup_label2) }
+ true | true | nil | 'new' | -> { expect(subject).to contain_exactly(label2, group_label2, subgroup_label2, sub_subgroup_label2) }
+ true | true | true | 'new' | -> { expect(subject).to contain_exactly(label2, group_label2, subgroup_label2, sub_subgroup_label2) }
+ end
+
+ with_them do
+ let(:params) do
+ {
+ include_ancestor_groups: include_ancestor_groups,
+ include_descendant_groups: include_descendant_groups,
+ only_group_labels: only_group_labels,
+ search_term: search_term
+ }
+ end
+
+ subject { resolve_labels(project, params) }
+
+ it { self.instance_exec(&test) }
+ end
+ end
+ end
+
+ def resolve_labels(parent, args = {}, context = { current_user: current_user })
+ resolve(described_class, obj: parent, args: args, ctx: context)
+ end
+end
diff --git a/spec/graphql/resolvers/merge_requests_resolver_spec.rb b/spec/graphql/resolvers/merge_requests_resolver_spec.rb
index 50b9243efa5..c5c368fc88f 100644
--- a/spec/graphql/resolvers/merge_requests_resolver_spec.rb
+++ b/spec/graphql/resolvers/merge_requests_resolver_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe Resolvers::MergeRequestsResolver do
include GraphqlHelpers
+ include SortingHelper
let_it_be(:project) { create(:project, :repository) }
let_it_be(:milestone) { create(:milestone, project: project) }
@@ -30,6 +31,16 @@ RSpec.describe Resolvers::MergeRequestsResolver do
end
describe '#resolve' do
+ # One for the initial auth, then MRs, and the load of project and project_feature (for further auth):
+ # SELECT MAX("project_authorizations"."access_level") AS maximum_access_level,
+ # "project_authorizations"."user_id" AS project_authorizations_user_id
+ # FROM "project_authorizations"
+ # WHERE "project_authorizations"."project_id" = 2 AND "project_authorizations"."user_id" = 2
+ # GROUP BY "project_authorizations"."user_id"
+ # SELECT "merge_requests".* FROM "merge_requests" WHERE "merge_requests"."target_project_id" = 2
+ # AND "merge_requests"."iid" = 1 ORDER BY "merge_requests"."id" DESC
+ # SELECT "projects".* FROM "projects" WHERE "projects"."id" = 2
+ # SELECT "project_features".* FROM "project_features" WHERE "project_features"."project_id" = 2
let(:queries_per_project) { 3 }
context 'no arguments' do
@@ -72,15 +83,17 @@ RSpec.describe Resolvers::MergeRequestsResolver do
expect(result).to contain_exactly(merge_request_1, merge_request_2, merge_request_3)
end
- it 'can batch-resolve merge requests from different projects' do
+ it 'can batch-resolve merge requests from different projects', :request_store, :use_clean_rails_memory_store_caching do
# 2 queries for project_authorizations, and 2 for merge_requests
- result = batch_sync(max_queries: queries_per_project * 2) do
- resolve_mr(project, iids: [iid_1]) +
- resolve_mr(project, iids: [iid_2]) +
- resolve_mr(other_project, iids: [other_iid])
+ results = batch_sync(max_queries: queries_per_project * 2) do
+ a = resolve_mr(project, iids: [iid_1])
+ b = resolve_mr(project, iids: [iid_2])
+ c = resolve_mr(other_project, iids: [other_iid])
+
+ [a, b, c].flat_map(&:to_a)
end
- expect(result).to contain_exactly(merge_request_1, merge_request_2, other_merge_request)
+ expect(results).to contain_exactly(merge_request_1, merge_request_2, other_merge_request)
end
it 'resolves an unknown iid to be empty' do
@@ -134,9 +147,9 @@ RSpec.describe Resolvers::MergeRequestsResolver do
it 'takes more than one argument' do
mrs = [merge_request_3, merge_request_4]
branches = mrs.map(&:target_branch)
- result = resolve_mr(project, target_branches: branches )
+ result = resolve_mr(project, target_branches: branches)
- expect(result.compact).to match_array(mrs)
+ expect(result).to match_array(mrs)
end
end
@@ -173,7 +186,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do
it 'returns merge requests merged between the given period' do
result = resolve_mr(project, merged_after: 20.days.ago, merged_before: 5.days.ago)
- expect(result).to eq([merge_request_1])
+ expect(result).to contain_exactly(merge_request_1)
end
it 'does not return anything' do
@@ -187,7 +200,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do
it 'filters merge requests by milestone title' do
result = resolve_mr(project, milestone_title: milestone.title)
- expect(result).to eq([merge_request_with_milestone])
+ expect(result).to contain_exactly(merge_request_with_milestone)
end
it 'does not find anything' do
@@ -203,18 +216,29 @@ RSpec.describe Resolvers::MergeRequestsResolver do
result = resolve_mr(project, source_branches: [merge_request_4.source_branch], state: 'locked')
- expect(result.compact).to contain_exactly(merge_request_4)
+ expect(result).to contain_exactly(merge_request_4)
end
end
describe 'sorting' do
+ let(:mrs) do
+ [
+ merge_request_with_milestone, merge_request_6, merge_request_5, merge_request_4,
+ merge_request_3, merge_request_2, merge_request_1
+ ]
+ end
+
context 'when sorting by created' do
it 'sorts merge requests ascending' do
- expect(resolve_mr(project, sort: 'created_asc')).to eq [merge_request_1, merge_request_2, merge_request_3, merge_request_4, merge_request_5, merge_request_6, merge_request_with_milestone]
+ expect(resolve_mr(project, sort: 'created_asc'))
+ .to match_array(mrs)
+ .and be_sorted(:created_at, :asc)
end
it 'sorts merge requests descending' do
- expect(resolve_mr(project, sort: 'created_desc')).to eq [merge_request_with_milestone, merge_request_6, merge_request_5, merge_request_4, merge_request_3, merge_request_2, merge_request_1]
+ expect(resolve_mr(project, sort: 'created_desc'))
+ .to match_array(mrs)
+ .and be_sorted(:created_at, :desc)
end
end
@@ -225,11 +249,19 @@ RSpec.describe Resolvers::MergeRequestsResolver do
end
it 'sorts merge requests ascending' do
- expect(resolve_mr(project, sort: :merged_at_asc)).to eq [merge_request_1, merge_request_3, merge_request_with_milestone, merge_request_6, merge_request_5, merge_request_4, merge_request_2]
+ expect(resolve_mr(project, sort: :merged_at_asc))
+ .to match_array(mrs)
+ .and be_sorted(->(mr) { [merged_at(mr), -mr.id] })
end
it 'sorts merge requests descending' do
- expect(resolve_mr(project, sort: :merged_at_desc)).to eq [merge_request_3, merge_request_1, merge_request_with_milestone, merge_request_6, merge_request_5, merge_request_4, merge_request_2]
+ expect(resolve_mr(project, sort: :merged_at_desc))
+ .to match_array(mrs)
+ .and be_sorted(->(mr) { [-merged_at(mr), -mr.id] })
+ end
+
+ def merged_at(mr)
+ nils_last(mr.metrics.merged_at)
end
context 'when label filter is given and the optimized_issuable_label_filter feature flag is off' do
diff --git a/spec/graphql/resolvers/package_details_resolver_spec.rb b/spec/graphql/resolvers/package_details_resolver_spec.rb
index 825b2aed40a..1bdc069b3bb 100644
--- a/spec/graphql/resolvers/package_details_resolver_spec.rb
+++ b/spec/graphql/resolvers/package_details_resolver_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe Resolvers::PackageDetailsResolver do
include GraphqlHelpers
+ include ::Gitlab::Graphql::Laziness
let_it_be_with_reload(:project) { create(:project) }
let_it_be(:user) { project.owner }
@@ -11,10 +12,10 @@ RSpec.describe Resolvers::PackageDetailsResolver do
describe '#resolve' do
let(:args) do
- { id: package.to_global_id.to_s }
+ { id: global_id_of(package) }
end
- subject { resolve(described_class, ctx: { current_user: user }, args: args).sync }
+ subject { force(resolve(described_class, ctx: { current_user: user }, args: args)) }
it { is_expected.to eq(package) }
end
diff --git a/spec/graphql/resolvers/packages_resolver_spec.rb b/spec/graphql/resolvers/packages_resolver_spec.rb
index 9aec2c7e036..bc0588daf7f 100644
--- a/spec/graphql/resolvers/packages_resolver_spec.rb
+++ b/spec/graphql/resolvers/packages_resolver_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Resolvers::PackagesResolver do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
- let_it_be(:project) { create(:project) }
+ let_it_be(:project) { create(:project, :public) }
let_it_be(:package) { create(:package, project: project) }
describe '#resolve' do
diff --git a/spec/graphql/resolvers/release_milestones_resolver_spec.rb b/spec/graphql/resolvers/release_milestones_resolver_spec.rb
index 5f66cba859d..f05069998d0 100644
--- a/spec/graphql/resolvers/release_milestones_resolver_spec.rb
+++ b/spec/graphql/resolvers/release_milestones_resolver_spec.rb
@@ -6,18 +6,19 @@ RSpec.describe Resolvers::ReleaseMilestonesResolver do
include GraphqlHelpers
let_it_be(:release) { create(:release, :with_milestones, milestones_count: 2) }
+ let_it_be(:current_user) { create(:user, developer_projects: [release.project]) }
let(:resolved) do
- resolve(described_class, obj: release)
+ resolve(described_class, obj: release, ctx: { current_user: current_user })
end
describe '#resolve' do
- it "returns an OffsetActiveRecordRelationConnection" do
- expect(resolved).to be_a(::Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection)
+ it "uses offset-pagination" do
+ expect(resolved).to be_a(::Gitlab::Graphql::Pagination::OffsetPaginatedRelation)
end
it "includes the release's milestones in the returned OffsetActiveRecordRelationConnection" do
- expect(resolved.items).to eq(release.milestones.order_by_dates_and_title)
+ expect(resolved.to_a).to eq(release.milestones.order_by_dates_and_title)
end
end
end
diff --git a/spec/graphql/resolvers/release_resolver_spec.rb b/spec/graphql/resolvers/release_resolver_spec.rb
index 04765fc68e9..782c9604f15 100644
--- a/spec/graphql/resolvers/release_resolver_spec.rb
+++ b/spec/graphql/resolvers/release_resolver_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe Resolvers::ReleaseResolver do
let(:args) { {} }
it 'raises an error' do
- expect { resolve_release }.to raise_error(ArgumentError, "missing keyword: :tag_name")
+ expect { resolve_release }.to raise_error(ArgumentError)
end
end
end
diff --git a/spec/graphql/resolvers/terraform/states_resolver_spec.rb b/spec/graphql/resolvers/terraform/states_resolver_spec.rb
index 64b515528cd..91d48cd782b 100644
--- a/spec/graphql/resolvers/terraform/states_resolver_spec.rb
+++ b/spec/graphql/resolvers/terraform/states_resolver_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Resolvers::Terraform::StatesResolver do
include GraphqlHelpers
- it { expect(described_class.type).to eq(Types::Terraform::StateType) }
+ it { expect(described_class).to have_nullable_graphql_type(Types::Terraform::StateType.connection_type) }
it { expect(described_class.null).to be_truthy }
describe '#resolve' do
@@ -31,3 +31,21 @@ RSpec.describe Resolvers::Terraform::StatesResolver do
end
end
end
+
+RSpec.describe Resolvers::Terraform::StatesResolver.single do
+ it { expect(described_class).to be < Resolvers::Terraform::StatesResolver }
+
+ describe 'arguments' do
+ subject { described_class.arguments[argument] }
+
+ describe 'name' do
+ let(:argument) { 'name' }
+
+ it do
+ expect(subject).to be_present
+ expect(subject.type.to_s).to eq('String!')
+ expect(subject.description).to be_present
+ end
+ end
+ end
+end
diff --git a/spec/graphql/types/ci/pipeline_type_spec.rb b/spec/graphql/types/ci/pipeline_type_spec.rb
index d435e337ad7..2a1e030480d 100644
--- a/spec/graphql/types/ci/pipeline_type_spec.rb
+++ b/spec/graphql/types/ci/pipeline_type_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Types::Ci::PipelineType do
id iid sha before_sha status detailed_status config_source duration
coverage created_at updated_at started_at finished_at committed_at
stages user retryable cancelable jobs source_job downstream
- upstream path project active user_permissions
+ upstream path project active user_permissions warnings
]
if Gitlab.ee?
diff --git a/spec/graphql/types/ci_configuration/sast/analyzers_entity_input_type_spec.rb b/spec/graphql/types/ci_configuration/sast/analyzers_entity_input_type_spec.rb
new file mode 100644
index 00000000000..ac18f8d53a1
--- /dev/null
+++ b/spec/graphql/types/ci_configuration/sast/analyzers_entity_input_type_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Types::CiConfiguration::Sast::AnalyzersEntityInputType do
+ it { expect(described_class.graphql_name).to eq('SastCiConfigurationAnalyzersEntityInput') }
+
+ it { expect(described_class.arguments.keys).to match_array(%w[enabled name variables]) }
+end
diff --git a/spec/graphql/types/ci_configuration/sast/analyzers_entity_type_spec.rb b/spec/graphql/types/ci_configuration/sast/analyzers_entity_type_spec.rb
new file mode 100644
index 00000000000..27f6703b429
--- /dev/null
+++ b/spec/graphql/types/ci_configuration/sast/analyzers_entity_type_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['SastCiConfigurationAnalyzersEntity'] do
+ let(:fields) { %i[name label enabled description variables] }
+
+ it { expect(described_class.graphql_name).to eq('SastCiConfigurationAnalyzersEntity') }
+
+ it { expect(described_class).to have_graphql_fields(fields) }
+end
diff --git a/spec/graphql/types/ci_configuration/sast/entity_input_type_spec.rb b/spec/graphql/types/ci_configuration/sast/entity_input_type_spec.rb
new file mode 100644
index 00000000000..cefcf64164a
--- /dev/null
+++ b/spec/graphql/types/ci_configuration/sast/entity_input_type_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Types::CiConfiguration::Sast::EntityInputType do
+ it { expect(described_class.graphql_name).to eq('SastCiConfigurationEntityInput') }
+
+ it { expect(described_class.arguments.keys).to match_array(%w[field defaultValue value]) }
+end
diff --git a/spec/graphql/types/ci_configuration/sast/entity_type_spec.rb b/spec/graphql/types/ci_configuration/sast/entity_type_spec.rb
new file mode 100644
index 00000000000..762798670a5
--- /dev/null
+++ b/spec/graphql/types/ci_configuration/sast/entity_type_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['SastCiConfigurationEntity'] do
+ let(:fields) { %i[field label description type options default_value value size] }
+
+ it { expect(described_class.graphql_name).to eq('SastCiConfigurationEntity') }
+
+ it { expect(described_class).to have_graphql_fields(fields) }
+end
diff --git a/spec/graphql/types/ci_configuration/sast/input_type_spec.rb b/spec/graphql/types/ci_configuration/sast/input_type_spec.rb
new file mode 100644
index 00000000000..9f9d1dea98f
--- /dev/null
+++ b/spec/graphql/types/ci_configuration/sast/input_type_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Types::CiConfiguration::Sast::InputType do
+ it { expect(described_class.graphql_name).to eq('SastCiConfigurationInput') }
+
+ it { expect(described_class.arguments.keys).to match_array(%w[global pipeline analyzers]) }
+end
diff --git a/spec/graphql/types/ci_configuration/sast/options_entity_spec.rb b/spec/graphql/types/ci_configuration/sast/options_entity_spec.rb
new file mode 100644
index 00000000000..c60c8b9c84a
--- /dev/null
+++ b/spec/graphql/types/ci_configuration/sast/options_entity_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['SastCiConfigurationOptionsEntity'] do
+ let(:fields) { %i[label value] }
+
+ it { expect(described_class.graphql_name).to eq('SastCiConfigurationOptionsEntity') }
+
+ it { expect(described_class).to have_graphql_fields(fields) }
+end
diff --git a/spec/graphql/types/ci_configuration/sast/type_spec.rb b/spec/graphql/types/ci_configuration/sast/type_spec.rb
new file mode 100644
index 00000000000..e7a8cd436e4
--- /dev/null
+++ b/spec/graphql/types/ci_configuration/sast/type_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['SastCiConfiguration'] do
+ let(:fields) { %i[global pipeline analyzers] }
+
+ it { expect(described_class.graphql_name).to eq('SastCiConfiguration') }
+
+ it { expect(described_class).to have_graphql_fields(fields) }
+end
diff --git a/spec/graphql/types/ci_configuration/sast/ui_component_size_enum_spec.rb b/spec/graphql/types/ci_configuration/sast/ui_component_size_enum_spec.rb
new file mode 100644
index 00000000000..23184df809f
--- /dev/null
+++ b/spec/graphql/types/ci_configuration/sast/ui_component_size_enum_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::CiConfiguration::Sast::UiComponentSizeEnum do
+ specify { expect(described_class.graphql_name).to eq('SastUiComponentSize') }
+
+ it 'exposes all sizes of ui components' do
+ expect(described_class.values.keys).to include(*%w[SMALL MEDIUM LARGE])
+ end
+end
diff --git a/spec/graphql/types/container_repository_sort_enum_spec.rb b/spec/graphql/types/container_repository_sort_enum_spec.rb
new file mode 100644
index 00000000000..eb936c6d3a1
--- /dev/null
+++ b/spec/graphql/types/container_repository_sort_enum_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['ContainerRepositorySort'] do
+ specify { expect(described_class.graphql_name).to eq('ContainerRepositorySort') }
+
+ it_behaves_like 'common sort values'
+
+ it 'exposes all the existing issue sort values' do
+ expect(described_class.values.keys).to include(
+ *%w[NAME_ASC NAME_DESC]
+ )
+ end
+end
diff --git a/spec/graphql/types/event_type_spec.rb b/spec/graphql/types/event_type_spec.rb
new file mode 100644
index 00000000000..10c3b5e18ca
--- /dev/null
+++ b/spec/graphql/types/event_type_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::EventType do
+ specify { expect(described_class.graphql_name).to eq('Event') }
+
+ specify { expect(described_class).to require_graphql_authorizations(:read_event) }
+
+ specify { expect(described_class).to have_graphql_fields(:id, :author, :action, :created_at, :updated_at) }
+end
diff --git a/spec/graphql/types/eventable_type_spec.rb b/spec/graphql/types/eventable_type_spec.rb
new file mode 100644
index 00000000000..c1c7bf6d65a
--- /dev/null
+++ b/spec/graphql/types/eventable_type_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::EventableType do
+ it 'exposes events field' do
+ expect(described_class).to have_graphql_fields(:events)
+ end
+end
diff --git a/spec/graphql/types/group_type_spec.rb b/spec/graphql/types/group_type_spec.rb
index de19e8b602a..bba702ba3e9 100644
--- a/spec/graphql/types/group_type_spec.rb
+++ b/spec/graphql/types/group_type_spec.rb
@@ -38,5 +38,7 @@ RSpec.describe GitlabSchema.types['Group'] do
it { is_expected.to have_graphql_resolver(Resolvers::GroupMembersResolver) }
end
- it_behaves_like 'a GraphQL type with labels'
+ it_behaves_like 'a GraphQL type with labels' do
+ let(:labels_resolver_arguments) { [:search_term, :includeAncestorGroups, :includeDescendantGroups, :onlyGroupLabels] }
+ end
end
diff --git a/spec/graphql/types/merge_request_state_event_enum_spec.rb b/spec/graphql/types/merge_request_state_event_enum_spec.rb
new file mode 100644
index 00000000000..94214b29755
--- /dev/null
+++ b/spec/graphql/types/merge_request_state_event_enum_spec.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['MergeRequestNewState'] do
+ it 'has the appropriate values' do
+ expect(described_class.values).to contain_exactly(
+ ['OPEN', have_attributes(value: 'reopen')],
+ ['CLOSED', have_attributes(value: 'close')]
+ )
+ end
+end
diff --git a/spec/graphql/types/packages/composer/details_type_spec.rb b/spec/graphql/types/packages/composer/details_type_spec.rb
deleted file mode 100644
index 2e4cb965ded..00000000000
--- a/spec/graphql/types/packages/composer/details_type_spec.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe GitlabSchema.types['PackageComposerDetails'] do
- it { expect(described_class.graphql_name).to eq('PackageComposerDetails') }
-
- it 'includes all the package fields' do
- expected_fields = %w[
- id name version created_at updated_at package_type tags project pipelines versions
- ]
-
- expect(described_class).to include_graphql_fields(*expected_fields)
- end
-
- it 'includes composer specific files' do
- expected_fields = %w[
- composer_metadatum
- ]
-
- expect(described_class).to include_graphql_fields(*expected_fields)
- end
-end
diff --git a/spec/graphql/types/packages/composer/metadatum_type_spec.rb b/spec/graphql/types/packages/composer/metadatum_type_spec.rb
index 0f47d8f1812..a950c10a41d 100644
--- a/spec/graphql/types/packages/composer/metadatum_type_spec.rb
+++ b/spec/graphql/types/packages/composer/metadatum_type_spec.rb
@@ -2,9 +2,7 @@
require 'spec_helper'
-RSpec.describe GitlabSchema.types['PackageComposerMetadatumType'] do
- it { expect(described_class.graphql_name).to eq('PackageComposerMetadatumType') }
-
+RSpec.describe GitlabSchema.types['ComposerMetadata'] do
it 'includes composer metadatum fields' do
expected_fields = %w[
target_sha composer_json
diff --git a/spec/graphql/types/packages/package_type_enum_spec.rb b/spec/graphql/types/packages/package_type_enum_spec.rb
index 407d5786f65..ccd91485e4b 100644
--- a/spec/graphql/types/packages/package_type_enum_spec.rb
+++ b/spec/graphql/types/packages/package_type_enum_spec.rb
@@ -4,6 +4,6 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['PackageTypeEnum'] do
it 'exposes all package types' do
- expect(described_class.values.keys).to contain_exactly(*%w[MAVEN NPM CONAN NUGET PYPI COMPOSER GENERIC GOLANG DEBIAN])
+ expect(described_class.values.keys).to contain_exactly(*%w[MAVEN NPM CONAN NUGET PYPI COMPOSER GENERIC GOLANG DEBIAN RUBYGEMS])
end
end
diff --git a/spec/graphql/types/packages/package_type_spec.rb b/spec/graphql/types/packages/package_type_spec.rb
index 7003a4d4d07..43289a019b3 100644
--- a/spec/graphql/types/packages/package_type_spec.rb
+++ b/spec/graphql/types/packages/package_type_spec.rb
@@ -3,11 +3,12 @@
require 'spec_helper'
RSpec.describe GitlabSchema.types['Package'] do
- it { expect(described_class.graphql_name).to eq('Package') }
-
it 'includes all the package fields' do
expected_fields = %w[
- id name version created_at updated_at package_type tags project pipelines versions
+ id name version package_type
+ created_at updated_at
+ project
+ tags pipelines versions
]
expect(described_class).to include_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/packages/package_without_versions_type_spec.rb b/spec/graphql/types/packages/package_without_versions_type_spec.rb
new file mode 100644
index 00000000000..faa79e588d5
--- /dev/null
+++ b/spec/graphql/types/packages/package_without_versions_type_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['PackageWithoutVersions'] do
+ it 'includes all the package fields' do
+ expected_fields = %w[
+ id name version created_at updated_at package_type tags project pipelines
+ ]
+
+ expect(described_class).to include_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index 9d0d7a3918a..9579ef8b99b 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -31,12 +31,171 @@ RSpec.describe GitlabSchema.types['Project'] do
container_expiration_policy service_desk_enabled service_desk_address
issue_status_counts terraform_states alert_management_integrations
container_repositories container_repositories_count
- pipeline_analytics squash_read_only
+ pipeline_analytics squash_read_only sast_ci_configuration
]
expect(described_class).to include_graphql_fields(*expected_fields)
end
+ describe 'sast_ci_configuration' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ before do
+ stub_licensed_features(security_dashboard: true)
+ project.add_developer(user)
+ allow(project.repository).to receive(:blob_data_at).and_return(gitlab_ci_yml_content)
+ end
+
+ include_context 'read ci configuration for sast enabled project'
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ sastCiConfiguration {
+ global {
+ nodes {
+ type
+ options {
+ nodes {
+ label
+ value
+ }
+ }
+ field
+ label
+ defaultValue
+ value
+ size
+ }
+ }
+ pipeline {
+ nodes {
+ type
+ options {
+ nodes {
+ label
+ value
+ }
+ }
+ field
+ label
+ defaultValue
+ value
+ size
+ }
+ }
+ analyzers {
+ nodes {
+ name
+ label
+ enabled
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
+
+ it "returns the project's sast configuration for global variables" do
+ secure_analyzers_prefix = subject.dig('data', 'project', 'sastCiConfiguration', 'global', 'nodes').first
+ expect(secure_analyzers_prefix['type']).to eq('string')
+ expect(secure_analyzers_prefix['field']).to eq('SECURE_ANALYZERS_PREFIX')
+ expect(secure_analyzers_prefix['label']).to eq('Image prefix')
+ expect(secure_analyzers_prefix['defaultValue']).to eq('registry.gitlab.com/gitlab-org/security-products/analyzers')
+ expect(secure_analyzers_prefix['value']).to eq('registry.gitlab.com/gitlab-org/security-products/analyzers')
+ expect(secure_analyzers_prefix['size']).to eq('LARGE')
+ expect(secure_analyzers_prefix['options']).to be_nil
+ end
+
+ it "returns the project's sast configuration for pipeline variables" do
+ pipeline_stage = subject.dig('data', 'project', 'sastCiConfiguration', 'pipeline', 'nodes').first
+ expect(pipeline_stage['type']).to eq('string')
+ expect(pipeline_stage['field']).to eq('stage')
+ expect(pipeline_stage['label']).to eq('Stage')
+ expect(pipeline_stage['defaultValue']).to eq('test')
+ expect(pipeline_stage['value']).to eq('test')
+ expect(pipeline_stage['size']).to eq('MEDIUM')
+ end
+
+ 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('brakeman')
+ expect(analyzer['label']).to eq('Brakeman')
+ expect(analyzer['enabled']).to eq(true)
+ end
+
+ context "with guest user" do
+ before do
+ project.add_guest(user)
+ end
+
+ context 'when project is private' do
+ let(:project) { create(:project, :private, :repository) }
+
+ it "returns no configuration" do
+ secure_analyzers_prefix = subject.dig('data', 'project', 'sastCiConfiguration')
+ expect(secure_analyzers_prefix).to be_nil
+ end
+ end
+
+ context 'when project is public' do
+ let(:project) { create(:project, :public, :repository) }
+
+ context 'when repository is accessible by everyone' do
+ it "returns the project's sast configuration for global variables" do
+ secure_analyzers_prefix = subject.dig('data', 'project', 'sastCiConfiguration', 'global', 'nodes').first
+
+ expect(secure_analyzers_prefix['type']).to eq('string')
+ expect(secure_analyzers_prefix['field']).to eq('SECURE_ANALYZERS_PREFIX')
+ end
+ end
+ end
+ end
+
+ context "with non-member user" do
+ before do
+ project.team.truncate
+ end
+
+ context 'when project is private' do
+ let(:project) { create(:project, :private, :repository) }
+
+ it "returns no configuration" do
+ secure_analyzers_prefix = subject.dig('data', 'project', 'sastCiConfiguration')
+ expect(secure_analyzers_prefix).to be_nil
+ end
+ end
+
+ context 'when project is public' do
+ let(:project) { create(:project, :public, :repository) }
+
+ context 'when repository is accessible by everyone' do
+ it "returns the project's sast configuration for global variables" do
+ secure_analyzers_prefix = subject.dig('data', 'project', 'sastCiConfiguration', 'global', 'nodes').first
+ expect(secure_analyzers_prefix['type']).to eq('string')
+ expect(secure_analyzers_prefix['field']).to eq('SECURE_ANALYZERS_PREFIX')
+ end
+ end
+
+ context 'when repository is accessible only by team members' do
+ it "returns no configuration" do
+ project.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED,
+ builds_access_level: ProjectFeature::DISABLED,
+ repository_access_level: ProjectFeature::PRIVATE)
+
+ secure_analyzers_prefix = subject.dig('data', 'project', 'sastCiConfiguration')
+ expect(secure_analyzers_prefix).to be_nil
+ end
+ end
+ end
+ end
+ end
+
describe 'issue field' do
subject { described_class.fields['issue'] }
@@ -159,6 +318,13 @@ RSpec.describe GitlabSchema.types['Project'] do
it { is_expected.to have_graphql_type(Types::ContainerExpirationPolicyType) }
end
+ describe 'terraform state field' do
+ subject { described_class.fields['terraformState'] }
+
+ it { is_expected.to have_graphql_type(Types::Terraform::StateType) }
+ it { is_expected.to have_graphql_resolver(Resolvers::Terraform::StatesResolver.single) }
+ end
+
describe 'terraform states field' do
subject { described_class.fields['terraformStates'] }
@@ -166,7 +332,9 @@ RSpec.describe GitlabSchema.types['Project'] do
it { is_expected.to have_graphql_resolver(Resolvers::Terraform::StatesResolver) }
end
- it_behaves_like 'a GraphQL type with labels'
+ it_behaves_like 'a GraphQL type with labels' do
+ let(:labels_resolver_arguments) { [:search_term, :includeAncestorGroups] }
+ end
describe 'jira_imports' do
subject { resolve_field(:jira_imports, project) }
diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb
index 3e716865e56..fea0a3bd37e 100644
--- a/spec/graphql/types/query_type_spec.rb
+++ b/spec/graphql/types/query_type_spec.rb
@@ -95,9 +95,9 @@ RSpec.describe GitlabSchema.types['Query'] do
it { is_expected.to have_graphql_type(Types::ContainerRepositoryDetailsType) }
end
- describe 'package_composer_details field' do
- subject { described_class.fields['packageComposerDetails'] }
+ describe 'package field' do
+ subject { described_class.fields['package'] }
- it { is_expected.to have_graphql_type(Types::Packages::Composer::DetailsType) }
+ it { is_expected.to have_graphql_type(Types::Packages::PackageType) }
end
end
diff --git a/spec/graphql/types/user_type_spec.rb b/spec/graphql/types/user_type_spec.rb
index 0eff33bb25b..5b3662383d8 100644
--- a/spec/graphql/types/user_type_spec.rb
+++ b/spec/graphql/types/user_type_spec.rb
@@ -10,6 +10,7 @@ RSpec.describe GitlabSchema.types['User'] do
it 'has the expected fields' do
expected_fields = %w[
id
+ bot
user_permissions
snippets
name
diff --git a/spec/helpers/analytics/unique_visits_helper_spec.rb b/spec/helpers/analytics/unique_visits_helper_spec.rb
index ff363e81ac7..b4b370c169d 100644
--- a/spec/helpers/analytics/unique_visits_helper_spec.rb
+++ b/spec/helpers/analytics/unique_visits_helper_spec.rb
@@ -9,19 +9,6 @@ RSpec.describe Analytics::UniqueVisitsHelper do
let(:target_id) { 'p_analytics_valuestream' }
let(:current_user) { create(:user) }
- before do
- stub_feature_flags(track_unique_visits: true)
- end
-
- it 'does not track visits if feature flag disabled' do
- stub_feature_flags(track_unique_visits: false)
- sign_in(current_user)
-
- expect_any_instance_of(Gitlab::Analytics::UniqueVisits).not_to receive(:track_visit)
-
- helper.track_visit(target_id)
- end
-
it 'does not track visit if user is not logged in' do
expect_any_instance_of(Gitlab::Analytics::UniqueVisits).not_to receive(:track_visit)
diff --git a/spec/helpers/application_settings_helper_spec.rb b/spec/helpers/application_settings_helper_spec.rb
index 479e2d7ef9d..2cd01451e0d 100644
--- a/spec/helpers/application_settings_helper_spec.rb
+++ b/spec/helpers/application_settings_helper_spec.rb
@@ -194,4 +194,33 @@ RSpec.describe ApplicationSettingsHelper do
it { is_expected.to be false }
end
end
+
+ describe '.kroki_available_formats' do
+ let(:application_setting) { build(:application_setting) }
+
+ before do
+ helper.instance_variable_set(:@application_setting, application_setting)
+ stub_application_setting(kroki_formats: { 'blockdiag' => true, 'bpmn' => false, 'excalidraw' => false })
+ end
+
+ it 'returns available formats correctly' do
+ expect(helper.kroki_available_formats).to eq([
+ {
+ name: 'kroki_formats_blockdiag',
+ label: 'BlockDiag (includes BlockDiag, SeqDiag, ActDiag, NwDiag, PacketDiag and RackDiag)',
+ value: true
+ },
+ {
+ name: 'kroki_formats_bpmn',
+ label: 'BPMN',
+ value: false
+ },
+ {
+ name: 'kroki_formats_excalidraw',
+ label: 'Excalidraw',
+ value: false
+ }
+ ])
+ end
+ end
end
diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb
index 00c4a1880de..b5d70af1336 100644
--- a/spec/helpers/auth_helper_spec.rb
+++ b/spec/helpers/auth_helper_spec.rb
@@ -73,12 +73,12 @@ RSpec.describe AuthHelper do
describe 'enabled_button_based_providers' do
before do
- allow(helper).to receive(:auth_providers) { [:twitter, :github, :google_oauth2] }
+ allow(helper).to receive(:auth_providers) { [:twitter, :github, :google_oauth2, :openid_connect] }
end
context 'all providers are enabled to sign in' do
it 'returns all the enabled providers from settings' do
- expect(helper.enabled_button_based_providers).to include('twitter', 'github', 'google_oauth2')
+ expect(helper.enabled_button_based_providers).to include('twitter', 'github', 'google_oauth2', 'openid_connect')
end
it 'puts google and github in the beginning' do
diff --git a/spec/helpers/commits_helper_spec.rb b/spec/helpers/commits_helper_spec.rb
index 8a570bf9a90..2f5f4c4596b 100644
--- a/spec/helpers/commits_helper_spec.rb
+++ b/spec/helpers/commits_helper_spec.rb
@@ -7,19 +7,38 @@ RSpec.describe CommitsHelper do
context 'when current_user exists' do
before do
allow(helper).to receive(:current_user).and_return(double('User'))
- allow(helper).to receive(:can_collaborate_with_project?).and_return(true)
end
it 'renders a div for Vue' do
- result = helper.revert_commit_link('_commit_', '_path_', pajamas: true)
+ result = helper.revert_commit_link
expect(result).to include('js-revert-commit-trigger')
end
+ end
+
+ context 'when current_user does not exist' do
+ before do
+ allow(helper).to receive(:current_user).and_return(nil)
+ end
+
+ it 'does not render anything' do
+ result = helper.revert_commit_link
+
+ expect(result).to be_nil
+ end
+ end
+ end
- it 'does not render a div for Vue' do
- result = helper.revert_commit_link('_commit_', '_path_')
+ describe '#cherry_pick_commit_link' do
+ context 'when current_user exists' do
+ before do
+ allow(helper).to receive(:current_user).and_return(double('User'))
+ end
- expect(result).not_to include('js-revert-commit-trigger')
+ it 'renders a div for Vue' do
+ result = helper.cherry_pick_commit_link
+
+ expect(result).to include('js-cherry-pick-commit-trigger')
end
end
@@ -29,7 +48,7 @@ RSpec.describe CommitsHelper do
end
it 'does not render anything' do
- result = helper.revert_commit_link(double('Commit'), '_path_')
+ result = helper.cherry_pick_commit_link
expect(result).to be_nil
end
@@ -157,4 +176,77 @@ RSpec.describe CommitsHelper do
expect(helper.commit_path(project, commit)).to eq(project_commit_path(project, commit))
end
end
+
+ describe "#conditionally_paginate_diff_files" do
+ let(:diffs_collection) { instance_double(Gitlab::Diff::FileCollection::Commit, diff_files: diff_files) }
+ let(:diff_files) { Gitlab::Git::DiffCollection.new(files) }
+ let(:page) { nil }
+
+ let(:files) do
+ Array.new(85).map do
+ { too_large: false, diff: "" }
+ end
+ end
+
+ let(:params) do
+ {
+ page: page
+ }
+ end
+
+ subject { helper.conditionally_paginate_diff_files(diffs_collection, paginate: paginate) }
+
+ before do
+ allow(helper).to receive(:params).and_return(params)
+ end
+
+ context "pagination is enabled" do
+ let(:paginate) { true }
+
+ it "has been paginated" do
+ expect(subject).to be_an(Array)
+ end
+
+ it "can change the number of items per page" do
+ commits = helper.conditionally_paginate_diff_files(diffs_collection, paginate: paginate, per: 10)
+
+ expect(commits).to be_an(Array)
+ expect(commits.size).to eq(10)
+ end
+
+ context "page 1" do
+ let(:page) { 1 }
+
+ it "has 20 diffs" do
+ expect(subject.size).to eq(75)
+ end
+ end
+
+ context "page 2" do
+ let(:page) { 2 }
+
+ it "has the remaining 10 diffs" do
+ expect(subject.size).to eq(10)
+ end
+ end
+ end
+
+ context "pagination is disabled" do
+ let(:paginate) { false }
+
+ it "returns a standard DiffCollection" do
+ expect(subject).to be_a(Gitlab::Git::DiffCollection)
+ end
+ end
+
+ context "feature flag is disabled" do
+ let(:paginate) { true }
+
+ it "returns a standard DiffCollection" do
+ stub_feature_flags(paginate_commit_view: false)
+
+ expect(subject).to be_a(Gitlab::Git::DiffCollection)
+ end
+ end
+ end
end
diff --git a/spec/helpers/container_registry_helper_spec.rb b/spec/helpers/container_registry_helper_spec.rb
index 6e6e8137b3e..49e56113dd8 100644
--- a/spec/helpers/container_registry_helper_spec.rb
+++ b/spec/helpers/container_registry_helper_spec.rb
@@ -5,8 +5,8 @@ require 'spec_helper'
RSpec.describe ContainerRegistryHelper do
using RSpec::Parameterized::TableSyntax
- describe '#limit_delete_tags_service?' do
- subject { helper.limit_delete_tags_service? }
+ describe '#container_registry_expiration_policies_throttling?' do
+ subject { helper.container_registry_expiration_policies_throttling? }
where(:feature_flag_enabled, :client_support, :expected_result) do
true | true | true
diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb
index 3580959fde0..20fa8d62884 100644
--- a/spec/helpers/diff_helper_spec.rb
+++ b/spec/helpers/diff_helper_spec.rb
@@ -169,9 +169,9 @@ RSpec.describe DiffHelper do
it "returns strings with marked inline diffs" do
marked_old_line, marked_new_line = mark_inline_diffs(old_line, new_line)
- expect(marked_old_line).to eq(%q{abc <span class="idiff left right deletion">&#39;def&#39;</span>})
+ expect(marked_old_line).to eq(%q{abc <span class="idiff left deletion">&#39;</span>def<span class="idiff right deletion">&#39;</span>})
expect(marked_old_line).to be_html_safe
- expect(marked_new_line).to eq(%q{abc <span class="idiff left right addition">&quot;def&quot;</span>})
+ expect(marked_new_line).to eq(%q{abc <span class="idiff left addition">&quot;</span>def<span class="idiff right addition">&quot;</span>})
expect(marked_new_line).to be_html_safe
end
@@ -358,4 +358,48 @@ RSpec.describe DiffHelper do
expect(diff_file_path_text(diff_file, max: 10)).to eq("...open.rb")
end
end
+
+ describe "#collapsed_diff_url" do
+ let(:params) do
+ {
+ controller: "projects/commit",
+ action: "show",
+ namespace_id: "foo",
+ project_id: "bar",
+ id: commit.sha
+ }
+ end
+
+ subject { helper.collapsed_diff_url(diff_file) }
+
+ it "returns a valid URL" do
+ allow(helper).to receive(:safe_params).and_return(params)
+
+ expect(subject).to match(/foo\/bar\/-\/commit\/#{commit.sha}\/diff_for_path/)
+ end
+ end
+
+ describe "#render_fork_suggestion" do
+ subject { helper.render_fork_suggestion }
+
+ before do
+ allow(helper).to receive(:current_user).and_return(current_user)
+ end
+
+ context "user signed in" do
+ let(:current_user) { build(:user) }
+
+ it "renders the partial" do
+ expect(helper).to receive(:render).with(partial: "projects/fork_suggestion").exactly(:once)
+
+ 5.times { subject }
+ end
+ end
+
+ context "guest" do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_nil }
+ end
+ end
end
diff --git a/spec/helpers/enable_search_settings_helper_spec.rb b/spec/helpers/enable_search_settings_helper_spec.rb
new file mode 100644
index 00000000000..c55c549ea51
--- /dev/null
+++ b/spec/helpers/enable_search_settings_helper_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe EnableSearchSettingsHelper do
+ describe '#enable_search_settings' do
+ def before_content
+ helper.content_for(:before_content)
+ end
+
+ it 'sets content for before_content' do
+ expect(before_content).to be_nil
+
+ locals = { container_class: 'test-container-class' }
+
+ helper.enable_search_settings(locals: locals)
+
+ expect(before_content).to eql(helper.render("shared/search_settings", locals))
+ end
+ end
+end
diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb
index c629643e248..264bad92d56 100644
--- a/spec/helpers/events_helper_spec.rb
+++ b/spec/helpers/events_helper_spec.rb
@@ -200,7 +200,13 @@ RSpec.describe EventsHelper do
it 'returns a project snippet note url' do
event.target = create(:note_on_project_snippet, note: 'keep going')
- expect(subject).to eq("#{project_base_url}/-/snippets/#{event.note_target.id}#note_#{event.target.id}")
+ expect(subject).to eq("#{project_snippet_url(event.note_target.project, event.note_target)}#note_#{event.target.id}")
+ end
+
+ it 'returns a personal snippet note url' do
+ event.target = create(:note_on_personal_snippet, note: 'keep going')
+
+ expect(subject).to eq("#{snippet_url(event.note_target)}#note_#{event.target.id}")
end
it 'returns a project issue url' do
diff --git a/spec/helpers/groups/group_members_helper_spec.rb b/spec/helpers/groups/group_members_helper_spec.rb
index d75124b6da7..99efc7963e6 100644
--- a/spec/helpers/groups/group_members_helper_spec.rb
+++ b/spec/helpers/groups/group_members_helper_spec.rb
@@ -23,13 +23,13 @@ RSpec.describe Groups::GroupMembersHelper do
end
end
- describe '#linked_groups_data_json' do
+ describe '#group_group_links_data_json' do
include_context 'group_group_link'
it 'matches json schema' do
- json = helper.linked_groups_data_json(shared_group.shared_with_group_links)
+ json = helper.group_group_links_data_json(shared_group.shared_with_group_links)
- expect(json).to match_schema('group_group_links')
+ expect(json).to match_schema('group_link/group_group_links')
end
end
@@ -81,13 +81,13 @@ RSpec.describe Groups::GroupMembersHelper do
expect(helper.group_members_list_data_attributes(group, present_members([group_member]))).to include({
members: helper.members_data_json(group, present_members([group_member])),
member_path: '/groups/foo-bar/-/group_members/:id',
- group_id: group.id,
+ source_id: group.id,
can_manage_members: 'true'
})
end
end
- describe '#linked_groups_list_data_attributes' do
+ describe '#group_group_links_list_data_attributes' do
include_context 'group_group_link'
before do
@@ -95,10 +95,10 @@ RSpec.describe Groups::GroupMembersHelper do
end
it 'returns expected hash' do
- expect(helper.linked_groups_list_data_attributes(shared_group)).to include({
- members: helper.linked_groups_data_json(shared_group.shared_with_group_links),
+ expect(helper.group_group_links_list_data_attributes(shared_group)).to include({
+ members: helper.group_group_links_data_json(shared_group.shared_with_group_links),
member_path: '/groups/foo-bar/-/group_links/:id',
- group_id: shared_group.id
+ source_id: shared_group.id
})
end
end
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 8eb1b7b3b3d..61aaa618c45 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -444,4 +444,82 @@ RSpec.describe GroupsHelper do
end
end
end
+
+ describe '#group_open_issues_count' do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:count_service) { Groups::OpenIssuesCountService }
+
+ before do
+ allow(helper).to receive(:current_user) { current_user }
+ end
+
+ context 'when cached_sidebar_open_issues_count feature flag is enabled' do
+ before do
+ stub_feature_flags(cached_sidebar_open_issues_count: true)
+ end
+
+ it 'returns count value from cache' do
+ allow_next_instance_of(count_service) do |service|
+ allow(service).to receive(:count).and_return(2500)
+ end
+
+ expect(helper.group_open_issues_count(group)).to eq('2.5k')
+ end
+ end
+
+ context 'when cached_sidebar_open_issues_count feature flag is disabled' do
+ before do
+ stub_feature_flags(cached_sidebar_open_issues_count: false)
+ end
+
+ it 'returns not cached issues count' do
+ allow(helper).to receive(:group_issues_count).and_return(2500)
+
+ expect(helper.group_open_issues_count(group)).to eq('2,500')
+ end
+ end
+ end
+
+ describe '#cached_open_group_issues_count' do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:group) { create(:group, name: 'group') }
+ let_it_be(:count_service) { Groups::OpenIssuesCountService }
+
+ before do
+ allow(helper).to receive(:current_user) { current_user }
+ end
+
+ it 'returns all digits for count value under 1000' do
+ allow_next_instance_of(count_service) do |service|
+ allow(service).to receive(:count).and_return(999)
+ end
+
+ expect(helper.cached_open_group_issues_count(group)).to eq('999')
+ end
+
+ it 'returns truncated digits for count value over 1000' do
+ allow_next_instance_of(count_service) do |service|
+ allow(service).to receive(:count).and_return(2300)
+ end
+
+ expect(helper.cached_open_group_issues_count(group)).to eq('2.3k')
+ end
+
+ it 'returns truncated digits for count value over 10000' do
+ allow_next_instance_of(count_service) do |service|
+ allow(service).to receive(:count).and_return(12560)
+ end
+
+ expect(helper.cached_open_group_issues_count(group)).to eq('12.6k')
+ end
+
+ it 'returns truncated digits for count value over 100000' do
+ allow_next_instance_of(count_service) do |service|
+ allow(service).to receive(:count).and_return(112560)
+ end
+
+ expect(helper.cached_open_group_issues_count(group)).to eq('112.6k')
+ end
+ end
end
diff --git a/spec/helpers/invite_members_helper_spec.rb b/spec/helpers/invite_members_helper_spec.rb
index 914d0931476..576021b37b3 100644
--- a/spec/helpers/invite_members_helper_spec.rb
+++ b/spec/helpers/invite_members_helper_spec.rb
@@ -7,11 +7,49 @@ RSpec.describe InviteMembersHelper do
let_it_be(:developer) { create(:user, developer_projects: [project]) }
let(:owner) { project.owner }
+ before do
+ helper.extend(Gitlab::Experimentation::ControllerConcern)
+ end
+
context 'with project' do
before do
assign(:project, project)
end
+ describe "#can_invite_members_for_project?" do
+ context 'when the user can_import_members' do
+ before do
+ allow(helper).to receive(:can_import_members?).and_return(true)
+ end
+
+ it 'returns true' do
+ expect(helper.can_invite_members_for_project?(project)).to eq true
+ expect(helper).to have_received(:can_import_members?)
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(invite_members_group_modal: false)
+ end
+
+ it 'returns false' do
+ expect(helper.can_invite_members_for_project?(project)).to eq false
+ expect(helper).not_to have_received(:can_import_members?)
+ end
+ end
+ end
+
+ context 'when the user can not invite members' do
+ before do
+ expect(helper).to receive(:can_import_members?).and_return(false)
+ end
+
+ it 'returns false' do
+ expect(helper.can_invite_members_for_project?(project)).to eq false
+ end
+ end
+ end
+
describe "#directly_invite_members?" do
context 'when the user is an owner' do
before do
@@ -80,6 +118,51 @@ RSpec.describe InviteMembersHelper do
context 'with group' do
let_it_be(:group) { create(:group) }
+ describe "#can_invite_members_for_group?" do
+ include Devise::Test::ControllerHelpers
+
+ let_it_be(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ allow(helper).to receive(:current_user) { user }
+ end
+
+ context 'when the user can_import_members' do
+ before do
+ allow(helper).to receive(:can?).with(user, :admin_group_member, group).and_return(true)
+ end
+
+ it 'returns true' do
+ expect(helper.can_invite_members_for_group?(group)).to eq true
+ expect(helper).to have_received(:can?).with(user, :admin_group_member, group)
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(invite_members_group_modal: false)
+ end
+
+ it 'returns false' do
+ stub_feature_flags(invite_members_group_modal: false)
+
+ expect(helper.can_invite_members_for_group?(group)).to eq false
+ expect(helper).not_to have_received(:can?)
+ end
+ end
+ end
+
+ context 'when the user can not invite members' do
+ before do
+ expect(helper).to receive(:can?).with(user, :admin_group_member, group).and_return(false)
+ end
+
+ it 'returns false' do
+ expect(helper.can_invite_members_for_group?(group)).to eq false
+ end
+ end
+ end
+
describe "#invite_group_members?" do
context 'when the user is an owner' do
before do
@@ -123,7 +206,6 @@ RSpec.describe InviteMembersHelper do
before do
allow(helper).to receive(:experiment_tracking_category_and_group) { '_track_property_' }
- allow(helper).to receive(:tracking_label).with(owner)
allow(helper).to receive(:current_user) { owner }
end
@@ -132,8 +214,7 @@ RSpec.describe InviteMembersHelper do
helper.dropdown_invite_members_link(form_model)
- expect(helper).to have_received(:experiment_tracking_category_and_group)
- .with(:invite_members_new_dropdown, subject: owner)
+ expect(helper).to have_received(:experiment_tracking_category_and_group).with(:invite_members_new_dropdown)
end
context 'with experiment enabled' do
diff --git a/spec/helpers/issuables_description_templates_helper_spec.rb b/spec/helpers/issuables_description_templates_helper_spec.rb
new file mode 100644
index 00000000000..42643b755f8
--- /dev/null
+++ b/spec/helpers/issuables_description_templates_helper_spec.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe IssuablesDescriptionTemplatesHelper, :clean_gitlab_redis_cache do
+ include_context 'project issuable templates context'
+
+ describe '#issuable_templates' do
+ let_it_be(:inherited_from) { nil }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:parent_group, reload: true) { create(:group) }
+ let_it_be(:project, reload: true) { create(:project, :custom_repo, files: issuable_template_files) }
+ let_it_be(:group_member) { create(:group_member, :developer, group: parent_group, user: user) }
+ let_it_be(:project_member) { create(:project_member, :developer, user: user, project: project) }
+
+ it 'returns empty hash when template type does not exist' do
+ expect(helper.issuable_templates(build(:project), 'non-existent-template-type')).to eq([])
+ end
+
+ context 'with cached issuable templates' do
+ before do
+ allow(Gitlab::Template::IssueTemplate).to receive(:template_names).and_return({})
+ allow(Gitlab::Template::MergeRequestTemplate).to receive(:template_names).and_return({})
+
+ helper.issuable_templates(project, 'issues')
+ helper.issuable_templates(project, 'merge_request')
+ end
+
+ it 'does not call TemplateFinder' do
+ expect(Gitlab::Template::IssueTemplate).not_to receive(:template_names)
+ expect(Gitlab::Template::MergeRequestTemplate).not_to receive(:template_names)
+ helper.issuable_templates(project, 'issues')
+ helper.issuable_templates(project, 'merge_request')
+ end
+ end
+
+ context 'when project has no parent group' do
+ it_behaves_like 'project issuable templates'
+ end
+
+ context 'when project has parent group' do
+ before do
+ project.update!(group: parent_group)
+ end
+
+ context 'when project parent group does not have a file template project' do
+ it_behaves_like 'project issuable templates'
+ end
+
+ context 'when project parent group has a file template project' do
+ let_it_be(:file_template_project) { create(:project, :custom_repo, group: parent_group, files: issuable_template_files) }
+ let_it_be(:group, reload: true) { create(:group, parent: parent_group) }
+ let_it_be(:project, reload: true) { create(:project, :custom_repo, group: group, files: issuable_template_files) }
+
+ before do
+ project.update!(group: group)
+ parent_group.update_columns(file_template_project_id: file_template_project.id)
+ end
+
+ it_behaves_like 'project issuable templates'
+ end
+ end
+ end
+
+ describe '#issuable_templates_names' do
+ let(:project) { double(Project, id: 21) }
+
+ let(:templates) do
+ [
+ { name: "another_issue_template", id: "another_issue_template", project_id: project.id },
+ { name: "custom_issue_template", id: "custom_issue_template", project_id: project.id }
+ ]
+ end
+
+ it 'returns project templates only' do
+ allow(helper).to receive(:ref_project).and_return(project)
+ allow(helper).to receive(:issuable_templates).and_return(templates)
+
+ expect(helper.issuable_templates_names(Issue.new)).to eq(%w[another_issue_template custom_issue_template])
+ end
+
+ context 'when there are not templates in the project' do
+ let(:templates) { {} }
+
+ it 'returns empty array' do
+ allow(helper).to receive(:ref_project).and_return(project)
+ allow(helper).to receive(:issuable_templates).and_return(templates)
+
+ expect(helper.issuable_templates_names(Issue.new)).to eq([])
+ end
+ end
+ end
+end
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index 57845904d32..d6b002b47eb 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -72,28 +72,38 @@ RSpec.describe IssuablesHelper do
let(:user) { create(:user) }
describe 'state text' do
- before do
- allow(helper).to receive(:issuables_count_for_state).and_return(42)
- end
-
- it 'returns "Open" when state is :opened' do
- expect(helper.issuables_state_counter_text(:issues, :opened, true))
- .to eq('<span>Open</span> <span class="badge badge-pill">42</span>')
- end
+ context 'when number of issuables can be generated' do
+ before do
+ allow(helper).to receive(:issuables_count_for_state).and_return(42)
+ end
- it 'returns "Closed" when state is :closed' do
- expect(helper.issuables_state_counter_text(:issues, :closed, true))
- .to eq('<span>Closed</span> <span class="badge badge-pill">42</span>')
+ it 'returns navigation with badges' do
+ expect(helper.issuables_state_counter_text(:issues, :opened, true))
+ .to eq('<span>Open</span> <span class="badge badge-pill">42</span>')
+ expect(helper.issuables_state_counter_text(:issues, :closed, true))
+ .to eq('<span>Closed</span> <span class="badge badge-pill">42</span>')
+ expect(helper.issuables_state_counter_text(:merge_requests, :merged, true))
+ .to eq('<span>Merged</span> <span class="badge badge-pill">42</span>')
+ expect(helper.issuables_state_counter_text(:merge_requests, :all, true))
+ .to eq('<span>All</span> <span class="badge badge-pill">42</span>')
+ end
end
- it 'returns "Merged" when state is :merged' do
- expect(helper.issuables_state_counter_text(:merge_requests, :merged, true))
- .to eq('<span>Merged</span> <span class="badge badge-pill">42</span>')
- end
+ context 'when count cannot be generated' do
+ before do
+ allow(helper).to receive(:issuables_count_for_state).and_return(-1)
+ end
- it 'returns "All" when state is :all' do
- expect(helper.issuables_state_counter_text(:merge_requests, :all, true))
- .to eq('<span>All</span> <span class="badge badge-pill">42</span>')
+ it 'returns avigation without badges' do
+ expect(helper.issuables_state_counter_text(:issues, :opened, true))
+ .to eq('<span>Open</span>')
+ expect(helper.issuables_state_counter_text(:issues, :closed, true))
+ .to eq('<span>Closed</span>')
+ expect(helper.issuables_state_counter_text(:merge_requests, :merged, true))
+ .to eq('<span>Merged</span>')
+ expect(helper.issuables_state_counter_text(:merge_requests, :all, true))
+ .to eq('<span>All</span>')
+ end
end
end
end
@@ -199,6 +209,7 @@ RSpec.describe IssuablesHelper do
markdownDocsPath: '/help/user/markdown',
lockVersion: issue.lock_version,
projectPath: @project.path,
+ projectId: @project.id,
projectNamespace: @project.namespace.path,
initialTitleHtml: issue.title,
initialTitleText: issue.title,
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index 1ed61bd3144..07e55e9b016 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -254,4 +254,31 @@ RSpec.describe IssuesHelper do
expect(helper.use_startup_call?).to eq(true)
end
end
+
+ describe '#issue_header_actions_data' do
+ let(:current_user) { create(:user) }
+
+ before do
+ allow(helper).to receive(:current_user).and_return(current_user)
+ allow(helper).to receive(:can?).and_return(true)
+ end
+
+ it 'returns expected result' do
+ expected = {
+ can_create_issue: "true",
+ can_reopen_issue: "true",
+ can_report_spam: "false",
+ can_update_issue: "true",
+ iid: issue.iid,
+ is_issue_author: "false",
+ issue_type: "issue",
+ new_issue_path: new_project_issue_path(project),
+ project_path: project.full_path,
+ report_abuse_path: new_abuse_report_path(user_id: issue.author.id, ref_url: issue_url(issue)),
+ submit_as_spam_path: mark_as_spam_project_issue_path(project, issue)
+ }
+
+ expect(helper.issue_header_actions_data(project, issue, current_user)).to include(expected)
+ end
+ end
end
diff --git a/spec/helpers/jira_connect_helper_spec.rb b/spec/helpers/jira_connect_helper_spec.rb
index a99072527c8..9695bed948b 100644
--- a/spec/helpers/jira_connect_helper_spec.rb
+++ b/spec/helpers/jira_connect_helper_spec.rb
@@ -4,12 +4,43 @@ require 'spec_helper'
RSpec.describe JiraConnectHelper do
describe '#jira_connect_app_data' do
- subject { helper.jira_connect_app_data }
+ let_it_be(:subscription) { create(:jira_connect_subscription) }
+ let(:user) { create(:user) }
- it 'includes Jira Connect app attributes' do
- is_expected.to include(
- :groups_path
- )
+ subject { helper.jira_connect_app_data([subscription]) }
+
+ context 'user is not logged in' do
+ before do
+ allow(view).to receive(:current_user).and_return(nil)
+ end
+
+ it 'includes Jira Connect app attributes' do
+ is_expected.to include(
+ :groups_path,
+ :subscriptions_path,
+ :users_path
+ )
+ end
+
+ it 'assigns users_path with value' do
+ expect(subject[:users_path]).to eq(jira_connect_users_path)
+ end
+
+ it 'passes group as "skip_groups" param' do
+ skip_groups_param = CGI.escape('skip_groups[]')
+
+ expect(subject[:groups_path]).to include("#{skip_groups_param}=#{subscription.namespace.id}")
+ end
+ end
+
+ context 'user is logged in' do
+ before do
+ allow(view).to receive(:current_user).and_return(user)
+ end
+
+ it 'assigns users_path to nil' do
+ expect(subject[:users_path]).to be_nil
+ end
end
end
end
diff --git a/spec/helpers/learn_gitlab_helper_spec.rb b/spec/helpers/learn_gitlab_helper_spec.rb
new file mode 100644
index 00000000000..f789eb9d940
--- /dev/null
+++ b/spec/helpers/learn_gitlab_helper_spec.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe LearnGitlabHelper do
+ include AfterNextHelpers
+ include Devise::Test::ControllerHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, name: LearnGitlab::PROJECT_NAME, namespace: user.namespace) }
+ let_it_be(:namespace) { project.namespace }
+
+ before do
+ project.add_developer(user)
+
+ allow(helper).to receive(:user).and_return(user)
+ allow_next_instance_of(LearnGitlab) do |learn_gitlab|
+ allow(learn_gitlab).to receive(:project).and_return(project)
+ end
+
+ OnboardingProgress.onboard(namespace)
+ OnboardingProgress.register(namespace, :git_write)
+ end
+
+ describe '.onboarding_actions_data' do
+ subject(:onboarding_actions_data) { helper.onboarding_actions_data(project) }
+
+ it 'has all actions' do
+ expect(onboarding_actions_data.keys).to contain_exactly(
+ :git_write,
+ :pipeline_created,
+ :merge_request_created,
+ :user_added,
+ :trial_started,
+ :required_mr_approvals_enabled,
+ :code_owners_enabled,
+ :security_scan_enabled
+ )
+ end
+
+ it 'sets correct path and completion status' do
+ expect(onboarding_actions_data[:git_write]).to eq({
+ url: project_issue_url(project, LearnGitlabHelper::ACTION_ISSUE_IDS[:git_write]),
+ completed: true
+ })
+ expect(onboarding_actions_data[:pipeline_created]).to eq({
+ url: project_issue_url(project, LearnGitlabHelper::ACTION_ISSUE_IDS[:pipeline_created]),
+ completed: false
+ })
+ end
+ end
+
+ describe '.learn_gitlab_experiment_enabled?' do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, namespace: user.namespace) }
+
+ let(:params) { { namespace_id: project.namespace.to_param, project_id: project } }
+
+ subject { helper.learn_gitlab_experiment_enabled?(project) }
+
+ where(:experiment_a, :experiment_b, :onboarding, :learn_gitlab_available, :result) do
+ true | false | true | true | true
+ false | true | true | true | true
+ false | false | true | true | false
+ true | true | true | false | false
+ true | true | false | true | false
+ end
+
+ with_them do
+ before do
+ stub_experiment_for_subject(learn_gitlab_a: experiment_a, learn_gitlab_b: experiment_b)
+ allow(OnboardingProgress).to receive(:onboarding?).with(project.namespace).and_return(onboarding)
+ allow_next(LearnGitlab, user).to receive(:available?).and_return(learn_gitlab_available)
+ end
+
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
+
+ it { is_expected.to eq(result) }
+ end
+
+ context 'when not signed in' do
+ it { is_expected.to eq(false) }
+ end
+ end
+ end
+end
diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb
index 821faaab194..fce4d560b2f 100644
--- a/spec/helpers/merge_requests_helper_spec.rb
+++ b/spec/helpers/merge_requests_helper_spec.rb
@@ -89,15 +89,5 @@ RSpec.describe MergeRequestsHelper do
total: user.assigned_open_merge_requests_count + user.review_requested_open_merge_requests_count
)
end
-
- context 'when merge_request_reviewers is disabled' do
- before do
- stub_feature_flags(merge_request_reviewers: false)
- end
-
- it 'returns review_requested as 0' do
- expect(subject[:review_requested]).to eq(0)
- end
- end
end
end
diff --git a/spec/helpers/notes_helper_spec.rb b/spec/helpers/notes_helper_spec.rb
index f9b3b535334..b8502cdf25e 100644
--- a/spec/helpers/notes_helper_spec.rb
+++ b/spec/helpers/notes_helper_spec.rb
@@ -316,4 +316,15 @@ RSpec.describe NotesHelper do
end
end
end
+
+ describe '#notes_data' do
+ let(:issue) { create(:issue, project: project) }
+
+ it 'sets last_fetched_at to 0 when start_at_zero is true' do
+ @project = project
+ @noteable = issue
+
+ expect(helper.notes_data(issue, true)[:lastFetchedAt]).to eq(0)
+ end
+ end
end
diff --git a/spec/helpers/notify_helper_spec.rb b/spec/helpers/notify_helper_spec.rb
index 9c9d745cb53..eb0f796038c 100644
--- a/spec/helpers/notify_helper_spec.rb
+++ b/spec/helpers/notify_helper_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe NotifyHelper do
include ActionView::Helpers::UrlHelper
+ using RSpec::Parameterized::TableSyntax
describe 'merge_request_reference_link' do
let(:project) { create(:project) }
@@ -27,6 +28,36 @@ RSpec.describe NotifyHelper do
end
end
+ describe '#invited_role_description' do
+ where(:role, :description) do
+ "Guest" | /As a guest/
+ "Reporter" | /As a reporter/
+ "Developer" | /As a developer/
+ "Maintainer" | /As a maintainer/
+ "Owner" | /As an owner/
+ "Minimal Access" | /As a user with minimal access/
+ end
+
+ with_them do
+ specify do
+ expect(helper.invited_role_description(role)).to match description
+ end
+ end
+ end
+
+ describe '#invited_to_description' do
+ where(:source, :description) do
+ "project" | /Projects can/
+ "group" | /Groups assemble/
+ end
+
+ with_them do
+ specify do
+ expect(helper.invited_to_description(source)).to match description
+ end
+ end
+ end
+
def reference_link(entity, url)
"<a href=\"#{url}\">#{entity.to_reference}</a>"
end
diff --git a/spec/helpers/operations_helper_spec.rb b/spec/helpers/operations_helper_spec.rb
index 801d5de79b1..5b0ce00063f 100644
--- a/spec/helpers/operations_helper_spec.rb
+++ b/spec/helpers/operations_helper_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe OperationsHelper do
it 'returns the correct values' do
expect(subject).to eq(
- 'alerts_setup_url' => help_page_path('operations/incident_management/alert_integrations.md', anchor: 'generic-http-endpoint'),
+ 'alerts_setup_url' => help_page_path('operations/incident_management/integrations.md', anchor: 'configuration'),
'alerts_usage_url' => project_alert_management_index_path(project),
'prometheus_form_path' => project_service_path(project, prometheus_service),
'prometheus_reset_key_path' => reset_alerting_token_project_settings_operations_path(project),
diff --git a/spec/helpers/projects/alert_management_helper_spec.rb b/spec/helpers/projects/alert_management_helper_spec.rb
index fd35c1ecab8..0df194e460a 100644
--- a/spec/helpers/projects/alert_management_helper_spec.rb
+++ b/spec/helpers/projects/alert_management_helper_spec.rb
@@ -28,8 +28,8 @@ RSpec.describe Projects::AlertManagementHelper do
expect(helper.alert_management_data(current_user, project)).to match(
'project-path' => project_path,
'enable-alert-management-path' => setting_path,
- 'alerts-help-url' => 'http://test.host/help/operations/incident_management/index.md',
- 'populating-alerts-help-url' => 'http://test.host/help/operations/incident_management/index.md#enable-alert-management',
+ 'alerts-help-url' => 'http://test.host/help/operations/incident_management/alerts.md',
+ 'populating-alerts-help-url' => 'http://test.host/help/operations/incident_management/integrations.md#configuration',
'empty-alert-svg-path' => match_asset_path('/assets/illustrations/alert-management-empty-state.svg'),
'user-can-enable-alert-management' => 'true',
'alert-management-enabled' => 'false',
@@ -113,7 +113,8 @@ RSpec.describe Projects::AlertManagementHelper do
'alert-id' => alert_id,
'project-path' => project_path,
'project-id' => project_id,
- 'project-issues-path' => issues_path
+ 'project-issues-path' => issues_path,
+ 'page' => 'OPERATIONS'
)
end
end
diff --git a/spec/helpers/projects/project_members_helper_spec.rb b/spec/helpers/projects/project_members_helper_spec.rb
index cc290367e34..5e0b4df7f7f 100644
--- a/spec/helpers/projects/project_members_helper_spec.rb
+++ b/spec/helpers/projects/project_members_helper_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Projects::ProjectMembersHelper do
+ include MembersPresentation
+
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
@@ -142,4 +144,58 @@ RSpec.describe Projects::ProjectMembersHelper do
it { is_expected.to be(false) }
end
end
+
+ describe 'project members' do
+ let_it_be(:project_members) { create_list(:project_member, 1, project: project) }
+
+ describe '#project_members_data_json' do
+ it 'matches json schema' do
+ expect(helper.project_members_data_json(project, present_members(project_members))).to match_schema('members')
+ end
+ end
+
+ describe '#project_members_list_data_attributes' do
+ let(:allow_admin_project) { true }
+
+ before do
+ allow(helper).to receive(:project_project_member_path).with(project, ':id').and_return('/foo-bar/-/project_members/:id')
+ end
+
+ it 'returns expected hash' do
+ expect(helper.project_members_list_data_attributes(project, present_members(project_members))).to include({
+ members: helper.project_members_data_json(project, present_members(project_members)),
+ member_path: '/foo-bar/-/project_members/:id',
+ source_id: project.id,
+ can_manage_members: true
+ })
+ end
+ end
+ end
+
+ describe 'project group links' do
+ let_it_be(:project_group_links) { create_list(:project_group_link, 1, project: project) }
+ let(:allow_admin_project) { true }
+
+ describe '#project_group_links_data_json' do
+ it 'matches json schema' do
+ expect(helper.project_group_links_data_json(project_group_links)).to match_schema('group_link/project_group_links')
+ end
+ end
+
+ describe '#project_group_links_list_data_attributes' do
+ before do
+ allow(helper).to receive(:project_group_link_path).with(project, ':id').and_return('/foo-bar/-/group_links/:id')
+ allow(helper).to receive(:can?).with(current_user, :admin_project_member, project).and_return(true)
+ end
+
+ it 'returns expected hash' do
+ expect(helper.project_group_links_list_data_attributes(project, project_group_links)).to include({
+ members: helper.project_group_links_data_json(project_group_links),
+ member_path: '/foo-bar/-/group_links/:id',
+ source_id: project.id,
+ can_manage_members: true
+ })
+ end
+ end
+ end
end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index b920e2e5600..303e3c78153 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe ProjectsHelper do
include ProjectForksHelper
+ include AfterNextHelpers
let_it_be_with_reload(:project) { create(:project) }
let_it_be_with_refind(:project_with_repo) { create(:project, :repository) }
@@ -398,6 +399,45 @@ RSpec.describe ProjectsHelper do
helper.send(:get_project_nav_tabs, project, user)
end
+ context 'Security & Compliance tabs' do
+ before do
+ stub_feature_flags(secure_security_and_compliance_configuration_page_on_ce: feature_flag_enabled)
+ allow(helper).to receive(:can?).with(user, :read_security_configuration, project).and_return(can_read_security_configuration)
+ end
+
+ context 'when user cannot read security configuration' do
+ let(:can_read_security_configuration) { false }
+
+ context 'when feature flag is disabled' do
+ let(:feature_flag_enabled) { false }
+
+ it { is_expected.not_to include(:security_configuration) }
+ end
+
+ context 'when feature flag is enabled' do
+ let(:feature_flag_enabled) { true }
+
+ it { is_expected.not_to include(:security_configuration) }
+ end
+ end
+
+ context 'when user can read security configuration' do
+ let(:can_read_security_configuration) { true }
+
+ context 'when feature flag is disabled' do
+ let(:feature_flag_enabled) { false }
+
+ it { is_expected.not_to include(:security_configuration) }
+ end
+
+ context 'when feature flag is enabled' do
+ let(:feature_flag_enabled) { true }
+
+ it { is_expected.to include(:security_configuration) }
+ end
+ end
+ end
+
context 'when builds feature is enabled' do
before do
allow(project).to receive(:builds_enabled?).and_return(true)
@@ -459,6 +499,20 @@ RSpec.describe ProjectsHelper do
it { is_expected.not_to include(:confluence) }
it { is_expected.to include(:wiki) }
end
+
+ context 'learn gitlab experiment' do
+ context 'when it is enabled' do
+ before do
+ expect(helper).to receive(:learn_gitlab_experiment_enabled?).with(project).and_return(true)
+ end
+
+ it { is_expected.to include(:learn_gitlab) }
+ end
+
+ context 'when it is not enabled' do
+ it { is_expected.not_to include(:learn_gitlab) }
+ end
+ end
end
describe '#can_view_operations_tab?' do
@@ -657,31 +711,6 @@ RSpec.describe ProjectsHelper do
end
end
- describe 'link_to_filter_repo' do
- subject { helper.link_to_filter_repo }
-
- it 'generates a hardcoded link to git filter-repo' do
- result = helper.link_to_filter_repo
- doc = Nokogiri::HTML.fragment(result)
-
- expect(doc.children.size).to eq(1)
-
- link = doc.children.first
-
- aggregate_failures do
- expect(result).to be_html_safe
-
- expect(link.name).to eq('a')
- expect(link[:target]).to eq('_blank')
- expect(link[:rel]).to eq('noopener noreferrer')
- expect(link[:href]).to eq('https://github.com/newren/git-filter-repo')
- expect(link.inner_html).to eq('git filter-repo')
-
- expect(result).to be_html_safe
- end
- end
- end
-
describe '#explore_projects_tab?' do
subject { helper.explore_projects_tab? }
@@ -854,16 +883,36 @@ RSpec.describe ProjectsHelper do
end
describe '#can_import_members?' do
- let(:owner) { project.owner }
+ context 'when user is project owner' do
+ before do
+ allow(helper).to receive(:current_user) { project.owner }
+ end
- it 'returns false if user cannot admin_project_member' do
- allow(helper).to receive(:current_user) { user }
- expect(helper.can_import_members?).to eq false
+ it 'returns true for owner of project' do
+ expect(helper.can_import_members?).to eq true
+ end
end
- it 'returns true if user can admin_project_member' do
- allow(helper).to receive(:current_user) { owner }
- expect(helper.can_import_members?).to eq true
+ context 'when user is not a project owner' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:user_project_role, :can_import) do
+ :maintainer | true
+ :developer | false
+ :reporter | false
+ :guest | false
+ end
+
+ with_them do
+ before do
+ project.add_role(user, user_project_role)
+ allow(helper).to receive(:current_user) { user }
+ end
+
+ it 'resolves if the user can import members' do
+ expect(helper.can_import_members?).to eq can_import
+ end
+ end
end
end
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index 2cb9d66ac63..a977f2c88c6 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -610,4 +610,35 @@ RSpec.describe SearchHelper do
end
end
end
+
+ describe '#search_sort_options' do
+ let(:user) { create(:user) }
+
+ mock_created_sort = [
+ {
+ title: _('Created date'),
+ sortable: true,
+ sortParam: {
+ asc: 'created_asc',
+ desc: 'created_desc'
+ }
+ },
+ {
+ title: _('Last updated'),
+ sortable: true,
+ sortParam: {
+ asc: 'updated_asc',
+ desc: 'updated_desc'
+ }
+ }
+ ]
+
+ before do
+ allow(self).to receive(:current_user).and_return(user)
+ end
+
+ it 'returns the correct data' do
+ expect(search_sort_options).to eq(mock_created_sort)
+ end
+ end
end
diff --git a/spec/helpers/sorting_helper_spec.rb b/spec/helpers/sorting_helper_spec.rb
index 2d581dfba37..f976fb098a8 100644
--- a/spec/helpers/sorting_helper_spec.rb
+++ b/spec/helpers/sorting_helper_spec.rb
@@ -50,24 +50,6 @@ RSpec.describe SortingHelper do
end
end
- describe '#search_sort_direction_button' do
- before do
- set_sorting_url 'test_label'
- end
-
- it 'keeps label filter param' do
- expect(search_sort_direction_button('created_asc')).to include('label_name=test_label')
- end
-
- it 'returns icon with sort-lowest when sort is asc' do
- expect(search_sort_direction_button('created_asc')).to include('sort-lowest')
- end
-
- it 'returns icon with sort-highest when sort is desc' do
- expect(search_sort_direction_button('created_desc')).to include('sort-highest')
- end
- end
-
def stub_controller_path(value)
allow(helper.controller).to receive(:controller_path).and_return(value)
end
diff --git a/spec/helpers/stat_anchors_helper_spec.rb b/spec/helpers/stat_anchors_helper_spec.rb
index c6556647bc8..0615baac3cb 100644
--- a/spec/helpers/stat_anchors_helper_spec.rb
+++ b/spec/helpers/stat_anchors_helper_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe StatAnchorsHelper do
let(:anchor) { anchor_klass.new(false, nil, nil, 'default') }
it 'returns the proper attributes' do
- expect(subject[:class]).to include('btn btn-default')
+ expect(subject[:class]).to include('gl-button btn btn-default')
end
end
@@ -29,7 +29,7 @@ RSpec.describe StatAnchorsHelper do
let(:anchor) { anchor_klass.new(false) }
it 'returns the proper attributes' do
- expect(subject[:class]).to include('btn btn-missing')
+ expect(subject[:class]).to include('gl-button btn btn-dashed')
end
end
end
diff --git a/spec/helpers/tree_helper_spec.rb b/spec/helpers/tree_helper_spec.rb
index 6cb9894e306..bc25a2fcdfc 100644
--- a/spec/helpers/tree_helper_spec.rb
+++ b/spec/helpers/tree_helper_spec.rb
@@ -19,94 +19,6 @@ RSpec.describe TreeHelper do
)
end
- describe '.render_tree' do
- before do
- @id = sha
- @path = ""
- @project = project
- @lfs_blob_ids = []
- end
-
- it 'displays all entries without a warning' do
- tree = repository.tree(sha, 'files')
-
- html = render_tree(tree)
-
- expect(html).not_to have_selector('.tree-truncated-warning')
- end
-
- it 'truncates entries and adds a warning' do
- stub_const('TreeHelper::FILE_LIMIT', 1)
- tree = repository.tree(sha, 'files')
-
- html = render_tree(tree)
-
- expect(html).to have_selector('.tree-truncated-warning', count: 1)
- expect(html).to have_selector('.tree-item-file-name', count: 1)
- end
- end
-
- describe '.fast_project_blob_path' do
- it 'generates the same path as project_blob_path' do
- blob_path = repository.tree(sha, 'with space').entries.first.path
- fast_path = fast_project_blob_path(project, blob_path)
- std_path = project_blob_path(project, blob_path)
-
- expect(fast_path).to eq(std_path)
- end
-
- it 'generates the same path with encoded file names' do
- tree = repository.tree(sha, 'encoding')
- blob_path = tree.entries.find { |entry| entry.path == 'encoding/テスト.txt' }.path
- fast_path = fast_project_blob_path(project, blob_path)
- std_path = project_blob_path(project, blob_path)
-
- expect(fast_path).to eq(std_path)
- end
-
- it 'respects a configured relative URL' do
- allow(Gitlab.config.gitlab).to receive(:relative_url_root).and_return('/gitlab/root')
- blob_path = repository.tree(sha, '').entries.first.path
- fast_path = fast_project_blob_path(project, blob_path)
-
- expect(fast_path).to start_with('/gitlab/root')
- end
-
- it 'encodes files starting with #' do
- filename = '#test-file'
- create_file(filename)
-
- fast_path = fast_project_blob_path(project, filename)
-
- expect(fast_path).to end_with('%23test-file')
- end
- end
-
- describe '.fast_project_tree_path' do
- let(:tree_path) { repository.tree(sha, 'with space').path }
- let(:fast_path) { fast_project_tree_path(project, tree_path) }
- let(:std_path) { project_tree_path(project, tree_path) }
-
- it 'generates the same path as project_tree_path' do
- expect(fast_path).to eq(std_path)
- end
-
- it 'respects a configured relative URL' do
- allow(Gitlab.config.gitlab).to receive(:relative_url_root).and_return('/gitlab/root')
-
- expect(fast_path).to start_with('/gitlab/root')
- end
-
- it 'encodes files starting with #' do
- filename = '#test-file'
- create_file(filename)
-
- fast_path = fast_project_tree_path(project, filename)
-
- expect(fast_path).to end_with('%23test-file')
- end
- end
-
describe 'flatten_tree' do
let(:tree) { repository.tree(sha, 'files') }
let(:root_path) { 'files' }
diff --git a/spec/helpers/user_callouts_helper_spec.rb b/spec/helpers/user_callouts_helper_spec.rb
index 250aedda906..b6607182461 100644
--- a/spec/helpers/user_callouts_helper_spec.rb
+++ b/spec/helpers/user_callouts_helper_spec.rb
@@ -222,4 +222,24 @@ RSpec.describe UserCalloutsHelper do
it { is_expected.to be true }
end
end
+
+ describe '.show_unfinished_tag_cleanup_callout?' do
+ subject { helper.show_unfinished_tag_cleanup_callout? }
+
+ before do
+ allow(helper).to receive(:user_dismissed?).with(described_class::UNFINISHED_TAG_CLEANUP_CALLOUT) { dismissed }
+ end
+
+ context 'when user has not dismissed' do
+ let(:dismissed) { false }
+
+ it { is_expected.to be true }
+ end
+
+ context 'when user dismissed' do
+ let(:dismissed) { true }
+
+ it { is_expected.to be false }
+ end
+ end
end
diff --git a/spec/initializers/lograge_spec.rb b/spec/initializers/lograge_spec.rb
index d5f9ef569c7..abb1673bb88 100644
--- a/spec/initializers/lograge_spec.rb
+++ b/spec/initializers/lograge_spec.rb
@@ -64,11 +64,11 @@ RSpec.describe 'lograge', type: :request do
)
expect(Lograge.formatter).to receive(:call)
- .with(a_hash_including(cpu_s: 0.11))
+ .with(a_hash_including(cpu_s: 0.111112))
.and_call_original
expect(Lograge.logger).to receive(:send)
- .with(anything, include('"cpu_s":0.11'))
+ .with(anything, include('"cpu_s":0.111112'))
.and_call_original
subject
@@ -89,6 +89,26 @@ RSpec.describe 'lograge', type: :request do
subject
end
+ context 'when logging memory allocations' do
+ include MemoryInstrumentationHelper
+
+ before do
+ skip_memory_instrumentation!
+ end
+
+ it 'logs memory usage metrics' do
+ expect(Lograge.formatter).to receive(:call)
+ .with(a_hash_including(:mem_objects))
+ .and_call_original
+
+ expect(Lograge.logger).to receive(:send)
+ .with(anything, include('"mem_objects":'))
+ .and_call_original
+
+ subject
+ end
+ end
+
it 'limits param size' do
expect(Lograge.formatter).to receive(:call)
.with(a_hash_including(params: limited_params))
diff --git a/spec/initializers/net_http_patch_spec.rb b/spec/initializers/net_http_patch_spec.rb
new file mode 100644
index 00000000000..e5205abbed2
--- /dev/null
+++ b/spec/initializers/net_http_patch_spec.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+require 'fast_spec_helper'
+
+RSpec.describe 'Net::HTTP patch proxy user and password encoding' do
+ let(:net_http) { Net::HTTP.new('hostname.example') }
+
+ describe '#proxy_user' do
+ subject { net_http.proxy_user }
+
+ it { is_expected.to eq(nil) }
+
+ context 'with http_proxy env' do
+ let(:http_proxy) { 'http://proxy.example:8000' }
+
+ before do
+ allow(ENV).to receive(:[]).and_call_original
+ allow(ENV).to receive(:[]).with('http_proxy').and_return(http_proxy)
+ end
+
+ it { is_expected.to eq(nil) }
+
+ context 'and user:password authentication' do
+ let(:http_proxy) { 'http://Y%5CX:R%25S%5D%20%3FX@proxy.example:8000' }
+
+ context 'when on multiuser safe platform' do
+ # linux, freebsd, darwin are considered multi user safe platforms
+ # See https://github.com/ruby/net-http/blob/v0.1.1/lib/net/http.rb#L1174-L1178
+
+ before do
+ allow(net_http).to receive(:environment_variable_is_multiuser_safe?).and_return(true)
+ end
+
+ it { is_expected.to eq 'Y\\X' }
+ end
+
+ context 'when not on multiuser safe platform' do
+ before do
+ allow(net_http).to receive(:environment_variable_is_multiuser_safe?).and_return(false)
+ end
+
+ it { is_expected.to be_nil }
+ end
+ end
+ end
+ end
+
+ describe '#proxy_pass' do
+ subject { net_http.proxy_pass }
+
+ it { is_expected.to eq(nil) }
+
+ context 'with http_proxy env' do
+ let(:http_proxy) { 'http://proxy.example:8000' }
+
+ before do
+ allow(ENV).to receive(:[]).and_call_original
+ allow(ENV).to receive(:[]).with('http_proxy').and_return(http_proxy)
+ end
+
+ it { is_expected.to eq(nil) }
+
+ context 'and user:password authentication' do
+ let(:http_proxy) { 'http://Y%5CX:R%25S%5D%20%3FX@proxy.example:8000' }
+
+ context 'when on multiuser safe platform' do
+ # linux, freebsd, darwin are considered multi user safe platforms
+ # See https://github.com/ruby/net-http/blob/v0.1.1/lib/net/http.rb#L1174-L1178
+
+ before do
+ allow(net_http).to receive(:environment_variable_is_multiuser_safe?).and_return(true)
+ end
+
+ it { is_expected.to eq 'R%S] ?X' }
+ end
+
+ context 'when not on multiuser safe platform' do
+ before do
+ allow(net_http).to receive(:environment_variable_is_multiuser_safe?).and_return(false)
+ end
+
+ it { is_expected.to be_nil }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/initializers/validate_puma_spec.rb b/spec/initializers/validate_puma_spec.rb
new file mode 100644
index 00000000000..9ff0ef2c319
--- /dev/null
+++ b/spec/initializers/validate_puma_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'validate puma' do
+ include RakeHelpers
+
+ subject do
+ load Rails.root.join('config/initializers/validate_puma.rb')
+ end
+
+ before do
+ stub_const('Puma', double)
+ allow(Gitlab::Runtime).to receive(:puma?).and_return(true)
+ allow(Puma).to receive_message_chain(:cli_config, :options).and_return(workers: workers)
+ end
+
+ context 'for .com' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ context 'when worker count is 0' do
+ let(:workers) { 0 }
+
+ specify { expect { subject }.to raise_error(String) }
+ end
+
+ context 'when worker count is > 0' do
+ let(:workers) { 2 }
+
+ specify { expect { subject }.not_to raise_error }
+ end
+ end
+
+ context 'for other environments' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(false)
+
+ allow(main_object).to receive(:warn)
+ end
+
+ context 'when worker count is 0' do
+ let(:workers) { 0 }
+
+ specify { expect { subject }.not_to raise_error }
+
+ it 'warns about running Puma in a Single mode' do
+ expect(main_object).to receive(:warn) do |warning|
+ expect(warning).to include('https://gitlab.com/groups/gitlab-org/-/epics/5303')
+ end
+
+ subject
+ end
+ end
+
+ context 'when worker count is > 0' do
+ let(:workers) { 2 }
+
+ specify { expect { subject }.not_to raise_error }
+
+ it 'does not issue a warning' do
+ expect(main_object).not_to receive(:warn)
+ end
+ end
+ end
+end
diff --git a/spec/javascripts/fly_out_nav_browser_spec.js b/spec/javascripts/fly_out_nav_browser_spec.js
index f92994594a9..12ea0e262bc 100644
--- a/spec/javascripts/fly_out_nav_browser_spec.js
+++ b/spec/javascripts/fly_out_nav_browser_spec.js
@@ -3,6 +3,7 @@
// see: https://gitlab.com/groups/gitlab-org/-/epics/895#what-if-theres-a-karma-spec-which-is-simply-unmovable-to-jest-ie-it-is-dependent-on-a-running-browser-environment
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
+import { SIDEBAR_COLLAPSED_CLASS } from '~/contextual_sidebar';
import {
calculateTop,
showSubLevelItems,
@@ -19,7 +20,6 @@ import {
setSidebar,
subItemsMouseLeave,
} from '~/fly_out_nav';
-import { SIDEBAR_COLLAPSED_CLASS } from '~/contextual_sidebar';
describe('Fly out sidebar navigation', () => {
let el;
diff --git a/spec/javascripts/matchers.js b/spec/javascripts/matchers.js
deleted file mode 100644
index ae005e152ed..00000000000
--- a/spec/javascripts/matchers.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import pixelmatch from 'pixelmatch';
-
-export default {
- toImageDiffEqual: () => {
- const getImageData = (img) => {
- const canvas = document.createElement('canvas');
- canvas.width = img.width;
- canvas.height = img.height;
- canvas.getContext('2d').drawImage(img, 0, 0);
- return canvas.getContext('2d').getImageData(0, 0, img.width, img.height).data;
- };
-
- return {
- compare(actual, expected, threshold = 0.1) {
- if (actual.height !== expected.height || actual.width !== expected.width) {
- return {
- pass: false,
- message: `Expected image dimensions (h x w) of ${expected.height}x${expected.width}.
- Received an image with ${actual.height}x${actual.width}`,
- };
- }
-
- const { width, height } = actual;
- const differentPixels = pixelmatch(
- getImageData(actual),
- getImageData(expected),
- null,
- width,
- height,
- { threshold },
- );
-
- return {
- pass: differentPixels < 20,
- message: `${differentPixels} pixels differ more than ${
- threshold * 100
- } percent between input and output.`,
- };
- },
- };
- },
-};
diff --git a/spec/javascripts/monitoring/components/dashboard_resize_browser_spec.js b/spec/javascripts/monitoring/components/dashboard_resize_browser_spec.js
index bbcdc0b879f..ec8d2778c1f 100644
--- a/spec/javascripts/monitoring/components/dashboard_resize_browser_spec.js
+++ b/spec/javascripts/monitoring/components/dashboard_resize_browser_spec.js
@@ -5,14 +5,14 @@
* https://gitlab.com/groups/gitlab-org/-/epics/895#what-if-theres-a-karma-spec-which-is-simply-unmovable-to-jest-ie-it-is-dependent-on-a-running-browser-environment
*/
-import Vue from 'vue';
import { createLocalVue } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import Vue from 'vue';
+import axios from '~/lib/utils/axios_utils';
import Dashboard from '~/monitoring/components/dashboard.vue';
import { createStore } from '~/monitoring/stores';
-import axios from '~/lib/utils/axios_utils';
-import { mockApiEndpoint } from '../mock_data';
import { metricsDashboardPayload, dashboardProps } from '../fixture_data';
+import { mockApiEndpoint } from '../mock_data';
import { setupStoreWithData } from '../store_utils';
const localVue = createLocalVue();
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index 59136de0b0d..be14d2ee7e7 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -2,20 +2,18 @@
jasmine/no-global-setup, no-underscore-dangle, no-console
*/
+import { config as testUtilsConfig } from '@vue/test-utils';
+import jasmineDiff from 'jasmine-diff';
import $ from 'jquery';
import 'core-js/features/set-immediate';
import 'vendor/jasmine-jquery';
import '~/commons';
import Vue from 'vue';
-import jasmineDiff from 'jasmine-diff';
-import { config as testUtilsConfig } from '@vue/test-utils';
+import { getDefaultAdapter } from '~/lib/utils/axios_utils';
import Translate from '~/vue_shared/translate';
-import { getDefaultAdapter } from '~/lib/utils/axios_utils';
import { FIXTURES_PATH, TEST_HOST } from './test_constants';
-import customMatchers from './matchers';
-
// Tech debt issue TBD
testUtilsConfig.logModifiedComponents = false;
@@ -58,7 +56,6 @@ beforeAll(() => {
inline: window.__karma__.config.color,
}),
);
- jasmine.addMatchers(customMatchers);
});
// globalize common libraries
@@ -81,14 +78,6 @@ window.addEventListener('unhandledrejection', (event) => {
console.error(event.reason.stack || event.reason);
});
-// HACK: Chrome 59 disconnects if there are too many synchronous tests in a row
-// because it appears to lock up the thread that communicates to Karma's socket
-// This async beforeEach gets called on every spec and releases the JS thread long
-// enough for the socket to continue to communicate.
-// The downside is that it creates a minor performance penalty in the time it takes
-// to run our unit tests.
-beforeEach((done) => done());
-
let longRunningTestTimeoutHandle;
beforeEach((done) => {
diff --git a/spec/lib/api/entities/merge_request_basic_spec.rb b/spec/lib/api/entities/merge_request_basic_spec.rb
index fe4c27b70ae..8572b067984 100644
--- a/spec/lib/api/entities/merge_request_basic_spec.rb
+++ b/spec/lib/api/entities/merge_request_basic_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe ::API::Entities::MergeRequestBasic do
let_it_be(:project) { create(:project, :public) }
let_it_be(:merge_request) { create(:merge_request) }
let_it_be(:labels) { create_list(:label, 3) }
- let_it_be(:merge_requests) { create_list(:labeled_merge_request, 10, :unique_branches, :with_diffs, labels: labels) }
+ let_it_be(:merge_requests) { create_list(:labeled_merge_request, 10, :unique_branches, labels: labels) }
# This mimics the behavior of the `Grape::Entity` serializer
def present(obj)
@@ -42,29 +42,14 @@ RSpec.describe ::API::Entities::MergeRequestBasic do
end
context 'reviewers' do
- context "when merge_request_reviewers FF is enabled" do
- before do
- stub_feature_flags(merge_request_reviewers: true)
- merge_request.reviewers = [user]
- end
-
- it 'includes assigned reviewers' do
- result = Gitlab::Json.parse(present(merge_request).to_json)
-
- expect(result['reviewers'][0]['username']).to eq user.username
- end
+ before do
+ merge_request.reviewers = [user]
end
- context "when merge_request_reviewers FF is disabled" do
- before do
- stub_feature_flags(merge_request_reviewers: false)
- end
-
- it 'does not include reviewers' do
- result = Gitlab::Json.parse(present(merge_request).to_json)
+ it 'includes assigned reviewers' do
+ result = Gitlab::Json.parse(present(merge_request).to_json)
- expect(result.keys).not_to include('reviewers')
- end
+ expect(result['reviewers'][0]['username']).to eq user.username
end
end
end
diff --git a/spec/lib/api/entities/user_spec.rb b/spec/lib/api/entities/user_spec.rb
index 99ffe0eb925..e35deeb6263 100644
--- a/spec/lib/api/entities/user_spec.rb
+++ b/spec/lib/api/entities/user_spec.rb
@@ -23,4 +23,16 @@ RSpec.describe API::Entities::User do
expect(subject).not_to include(:created_at)
end
+
+ it 'exposes user as not a bot' do
+ expect(subject[:bot]).to be_falsey
+ end
+
+ context 'with bot user' do
+ let(:user) { create(:user, :security_bot) }
+
+ it 'exposes user as a bot' do
+ expect(subject[:bot]).to eq(true)
+ end
+ end
end
diff --git a/spec/lib/api/support/git_access_actor_spec.rb b/spec/lib/api/support/git_access_actor_spec.rb
index 143cc6e56ee..a09cabf4cd7 100644
--- a/spec/lib/api/support/git_access_actor_spec.rb
+++ b/spec/lib/api/support/git_access_actor_spec.rb
@@ -152,6 +152,10 @@ RSpec.describe API::Support::GitAccessActor do
end
describe '#update_last_used_at!' do
+ before do
+ stub_feature_flags(disable_ssh_key_used_tracking: false)
+ end
+
context 'when initialized with a User' do
let(:user) { build(:user) }
@@ -170,6 +174,14 @@ RSpec.describe API::Support::GitAccessActor do
subject.update_last_used_at!
end
+
+ it 'does not update `last_used_at` when the functionality is disabled' do
+ stub_feature_flags(disable_ssh_key_used_tracking: true)
+
+ expect(key).not_to receive(:update_last_used_at)
+
+ subject.update_last_used_at!
+ end
end
end
end
diff --git a/spec/lib/atlassian/jira_connect/client_spec.rb b/spec/lib/atlassian/jira_connect/client_spec.rb
index 21ee40f22fe..5c8d4282118 100644
--- a/spec/lib/atlassian/jira_connect/client_spec.rb
+++ b/spec/lib/atlassian/jira_connect/client_spec.rb
@@ -18,15 +18,15 @@ RSpec.describe Atlassian::JiraConnect::Client do
end
end
- around do |example|
- freeze_time { example.run }
- end
-
describe '.generate_update_sequence_id' do
- it 'returns monotonic_time converted it to integer' do
- allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(1.0)
+ it 'returns unix time in microseconds as integer', :aggregate_failures do
+ travel_to(Time.utc(1970, 1, 1, 0, 0, 1)) do
+ expect(described_class.generate_update_sequence_id).to eq(1000)
+ end
- expect(described_class.generate_update_sequence_id).to eq(1)
+ travel_to(Time.utc(1970, 1, 1, 0, 0, 5)) do
+ expect(described_class.generate_update_sequence_id).to eq(5000)
+ end
end
end
@@ -238,22 +238,6 @@ RSpec.describe Atlassian::JiraConnect::Client do
expect(response['errorMessages']).to eq(%w(X Y Z))
end
end
-
- it 'does not call the API if the feature flag is not enabled' do
- stub_feature_flags(jira_sync_deployments: false)
-
- expect(subject).not_to receive(:post)
-
- subject.send(:store_deploy_info, project: project, deployments: deployments)
- end
-
- it 'does call the API if the feature flag enabled for the project' do
- stub_feature_flags(jira_sync_deployments: project)
-
- expect(subject).to receive(:post).with('/rest/deployments/0.1/bulk', { deployments: Array }).and_call_original
-
- subject.send(:store_deploy_info, project: project, deployments: deployments)
- end
end
describe '#store_ff_info' do
@@ -319,24 +303,6 @@ RSpec.describe Atlassian::JiraConnect::Client do
expect(response['errorMessages']).to eq(['a: X', 'a: Y', 'b: Z'])
end
end
-
- it 'does not call the API if the feature flag is not enabled' do
- stub_feature_flags(jira_sync_feature_flags: false)
-
- expect(subject).not_to receive(:post)
-
- subject.send(:store_ff_info, project: project, feature_flags: feature_flags)
- end
-
- it 'does call the API if the feature flag enabled for the project' do
- stub_feature_flags(jira_sync_feature_flags: project)
-
- expect(subject).to receive(:post).with('/rest/featureflags/0.1/bulk', {
- flags: Array, properties: Hash
- }).and_call_original
-
- subject.send(:store_ff_info, project: project, feature_flags: feature_flags)
- end
end
describe '#store_build_info' do
@@ -384,24 +350,6 @@ RSpec.describe Atlassian::JiraConnect::Client do
subject.send(:store_build_info, project: project, pipelines: pipelines.take(1))
end
- it 'does not call the API if the feature flag is not enabled' do
- stub_feature_flags(jira_sync_builds: false)
-
- expect(subject).not_to receive(:post)
-
- subject.send(:store_build_info, project: project, pipelines: pipelines)
- end
-
- it 'does call the API if the feature flag enabled for the project' do
- stub_feature_flags(jira_sync_builds: project)
-
- expect(subject).to receive(:post)
- .with('/rest/builds/0.1/bulk', { builds: Array })
- .and_call_original
-
- subject.send(:store_build_info, project: project, pipelines: pipelines)
- end
-
context 'there are errors' do
let(:failures) do
[{ errors: [{ message: 'X' }, { message: 'Y' }] }, { errors: [{ message: 'Z' }] }]
diff --git a/spec/lib/atlassian/jira_connect/serializers/feature_flag_entity_spec.rb b/spec/lib/atlassian/jira_connect/serializers/feature_flag_entity_spec.rb
index 964801338cf..2d12cd1ed0a 100644
--- a/spec/lib/atlassian/jira_connect/serializers/feature_flag_entity_spec.rb
+++ b/spec/lib/atlassian/jira_connect/serializers/feature_flag_entity_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Atlassian::JiraConnect::Serializers::FeatureFlagEntity do
subject { described_class.represent(feature_flag) }
context 'when the feature flag does not belong to any Jira issue' do
- let_it_be(:feature_flag) { create(:operations_feature_flag) }
+ let_it_be(:feature_flag) { create(:operations_feature_flag, project: project) }
describe '#issue_keys' do
it 'is empty' do
@@ -30,7 +30,7 @@ RSpec.describe Atlassian::JiraConnect::Serializers::FeatureFlagEntity do
context 'when the feature flag does belong to a Jira issue' do
let(:feature_flag) do
- create(:operations_feature_flag, description: 'THING-123')
+ create(:operations_feature_flag, project: project, description: 'THING-123')
end
describe '#issue_keys' do
@@ -66,6 +66,7 @@ RSpec.describe Atlassian::JiraConnect::Serializers::FeatureFlagEntity do
end
it 'has the correct summary' do
+ expect(entity.dig('summary', 'url')).to eq "http://localhost/#{project.full_path}/-/feature_flags/#{feature_flag.iid}/edit"
expect(entity.dig('summary', 'status')).to eq(
'enabled' => true,
'defaultValue' => '',
diff --git a/spec/lib/backup/files_spec.rb b/spec/lib/backup/files_spec.rb
index 450e396a389..92de191da2d 100644
--- a/spec/lib/backup/files_spec.rb
+++ b/spec/lib/backup/files_spec.rb
@@ -21,10 +21,6 @@ RSpec.describe Backup::Files do
allow(File).to receive(:realpath).with("/var/gitlab-pages").and_return("/var/gitlab-pages")
allow(File).to receive(:realpath).with("/var/gitlab-pages/..").and_return("/var")
- allow_any_instance_of(String).to receive(:color) do |string, _color|
- string
- end
-
allow_any_instance_of(described_class).to receive(:progress).and_return(progress)
end
@@ -150,7 +146,7 @@ RSpec.describe Backup::Files do
it 'excludes tmp dirs from rsync' do
expect(Gitlab::Popen).to receive(:popen)
- .with(%w(rsync -a --delete --exclude=lost+found --exclude=/@pages.tmp /var/gitlab-pages /var/gitlab-backup))
+ .with(%w(rsync -a --delete --exclude=lost+found --exclude=/gitlab-pages/@pages.tmp /var/gitlab-pages /var/gitlab-backup))
.and_return(['', 0])
subject.dump
@@ -158,7 +154,7 @@ RSpec.describe Backup::Files do
it 'retries if rsync fails due to vanishing files' do
expect(Gitlab::Popen).to receive(:popen)
- .with(%w(rsync -a --delete --exclude=lost+found --exclude=/@pages.tmp /var/gitlab-pages /var/gitlab-backup))
+ .with(%w(rsync -a --delete --exclude=lost+found --exclude=/gitlab-pages/@pages.tmp /var/gitlab-pages /var/gitlab-backup))
.and_return(['rsync failed', 24], ['', 0])
expect do
@@ -168,7 +164,7 @@ RSpec.describe Backup::Files do
it 'raises an error and outputs an error message if rsync failed' do
allow(Gitlab::Popen).to receive(:popen)
- .with(%w(rsync -a --delete --exclude=lost+found --exclude=/@pages.tmp /var/gitlab-pages /var/gitlab-backup))
+ .with(%w(rsync -a --delete --exclude=lost+found --exclude=/gitlab-pages/@pages.tmp /var/gitlab-pages /var/gitlab-backup))
.and_return(['rsync failed', 1])
expect do
@@ -186,8 +182,8 @@ RSpec.describe Backup::Files do
expect(subject.exclude_dirs(:tar)).to eq(['--exclude=lost+found', '--exclude=./@pages.tmp'])
end
- it 'prepends a leading slash to rsync excludes' do
- expect(subject.exclude_dirs(:rsync)).to eq(['--exclude=lost+found', '--exclude=/@pages.tmp'])
+ it 'prepends a leading slash and app_files_dir basename to rsync excludes' do
+ expect(subject.exclude_dirs(:rsync)).to eq(['--exclude=lost+found', '--exclude=/gitlab-pages/@pages.tmp'])
end
end
diff --git a/spec/lib/banzai/filter/asset_proxy_filter_spec.rb b/spec/lib/banzai/filter/asset_proxy_filter_spec.rb
index 1f886059bf6..81aa8d35ebc 100644
--- a/spec/lib/banzai/filter/asset_proxy_filter_spec.rb
+++ b/spec/lib/banzai/filter/asset_proxy_filter_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe Banzai::Filter::AssetProxyFilter do
stub_application_setting(asset_proxy_enabled: true)
stub_application_setting(asset_proxy_secret_key: 'shared-secret')
stub_application_setting(asset_proxy_url: 'https://assets.example.com')
- stub_application_setting(asset_proxy_whitelist: %w(gitlab.com *.mydomain.com))
+ stub_application_setting(asset_proxy_allowlist: %w(gitlab.com *.mydomain.com))
described_class.initialize_settings
@@ -39,16 +39,26 @@ RSpec.describe Banzai::Filter::AssetProxyFilter do
expect(Gitlab.config.asset_proxy.domain_regexp).to eq(/^(gitlab\.com|.*?\.mydomain\.com)$/i)
end
- context 'when whitelist is empty' do
+ context 'when allowlist is empty' do
it 'defaults to the install domain' do
stub_application_setting(asset_proxy_enabled: true)
- stub_application_setting(asset_proxy_whitelist: [])
+ stub_application_setting(asset_proxy_allowlist: [])
described_class.initialize_settings
expect(Gitlab.config.asset_proxy.allowlist).to eq [Gitlab.config.gitlab.host]
end
end
+
+ it 'supports deprecated whitelist settings' do
+ stub_application_setting(asset_proxy_enabled: true)
+ stub_application_setting(asset_proxy_whitelist: %w(foo.com bar.com))
+ stub_application_setting(asset_proxy_allowlist: [])
+
+ described_class.initialize_settings
+
+ expect(Gitlab.config.asset_proxy.allowlist).to eq %w(foo.com bar.com)
+ end
end
context 'when properly configured' do
diff --git a/spec/lib/banzai/filter/custom_emoji_filter_spec.rb b/spec/lib/banzai/filter/custom_emoji_filter_spec.rb
new file mode 100644
index 00000000000..ca8c9750e7f
--- /dev/null
+++ b/spec/lib/banzai/filter/custom_emoji_filter_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Banzai::Filter::CustomEmojiFilter do
+ include FilterSpecHelper
+
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:custom_emoji) { create(:custom_emoji, name: 'tanuki', group: group) }
+ let_it_be(:custom_emoji2) { create(:custom_emoji, name: 'happy_tanuki', group: group, file: 'https://foo.bar/happy.png') }
+
+ it 'replaces supported name custom emoji' do
+ doc = filter('<p>:tanuki:</p>', project: project)
+
+ expect(doc.css('gl-emoji').first.attributes['title'].value).to eq('tanuki')
+ expect(doc.css('gl-emoji img').size).to eq 1
+ end
+
+ it 'ignores non existent custom emoji' do
+ exp = act = '<p>:foo:</p>'
+ doc = filter(act)
+
+ expect(doc.to_html).to match Regexp.escape(exp)
+ end
+
+ it 'correctly uses the custom emoji URL' do
+ doc = filter('<p>:tanuki:</p>')
+
+ expect(doc.css('img').first.attributes['src'].value).to eq(custom_emoji.file)
+ end
+
+ it 'matches with adjacent text' do
+ doc = filter('tanuki (:tanuki:)')
+
+ expect(doc.css('img').size).to eq 1
+ end
+
+ it 'matches multiple same custom emoji' do
+ doc = filter(':tanuki: :tanuki:')
+
+ expect(doc.css('img').size).to eq 2
+ end
+
+ it 'matches multiple custom emoji' do
+ doc = filter(':tanuki: (:happy_tanuki:)')
+
+ expect(doc.css('img').size).to eq 2
+ end
+
+ it 'does not match enclosed colons' do
+ doc = filter('tanuki:tanuki:')
+
+ expect(doc.css('img').size).to be 0
+ end
+
+ it 'keeps whitespace intact' do
+ doc = filter('This deserves a :tanuki:, big time.')
+
+ expect(doc.to_html).to match(/^This deserves a <gl-emoji.+>, big time\.\z/)
+ end
+
+ it 'does not match emoji in a string' do
+ doc = filter("'2a00:tanuki:100::1'")
+
+ expect(doc.css('gl-emoji').size).to eq 0
+ end
+
+ it 'does not do N+1 query' do
+ create(:custom_emoji, name: 'party-parrot', group: group)
+
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ filter('<p>:tanuki:</p>')
+ end
+
+ expect do
+ filter('<p>:tanuki: :party-parrot:</p>')
+ end.not_to exceed_all_query_limit(control_count.count)
+ end
+end
diff --git a/spec/lib/banzai/filter/feature_flag_reference_filter_spec.rb b/spec/lib/banzai/filter/feature_flag_reference_filter_spec.rb
new file mode 100644
index 00000000000..2d7089853cf
--- /dev/null
+++ b/spec/lib/banzai/filter/feature_flag_reference_filter_spec.rb
@@ -0,0 +1,223 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Banzai::Filter::FeatureFlagReferenceFilter do
+ include FilterSpecHelper
+
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:feature_flag) { create(:operations_feature_flag, project: project) }
+ let_it_be(:reference) { feature_flag.to_reference }
+
+ it 'requires project context' do
+ expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
+ end
+
+ %w(pre code a style).each do |elem|
+ it "ignores valid references contained inside '#{elem}' element" do
+ exp = act = "<#{elem}>Feature Flag #{reference}</#{elem}>"
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'with internal reference' do
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).to eq urls.edit_project_feature_flag_url(project, feature_flag)
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("Feature Flag (#{reference}.)")
+
+ expect(doc.to_html).to match(%r{\(<a.+>#{Regexp.escape(reference)}</a>\.\)})
+ end
+
+ it 'ignores invalid feature flag IIDs' do
+ exp = act = "Check [feature_flag:#{non_existing_record_id}]"
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+
+ it 'includes a title attribute' do
+ doc = reference_filter("Feature Flag #{reference}")
+
+ expect(doc.css('a').first.attr('title')).to eq feature_flag.name
+ end
+
+ it 'escapes the title attribute' do
+ allow(feature_flag).to receive(:name).and_return(%{"></a>whatever<a title="})
+ doc = reference_filter("Feature Flag #{reference}")
+
+ expect(doc.text).to eq "Feature Flag #{reference}"
+ end
+
+ it 'includes default classes' do
+ doc = reference_filter("Feature Flag #{reference}")
+
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-feature_flag has-tooltip'
+ end
+
+ it 'includes a data-project attribute' do
+ doc = reference_filter("Feature Flag #{reference}")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-project')
+ expect(link.attr('data-project')).to eq project.id.to_s
+ end
+
+ it 'includes a data-feature-flag attribute' do
+ doc = reference_filter("See #{reference}")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-feature-flag')
+ expect(link.attr('data-feature-flag')).to eq feature_flag.id.to_s
+ end
+
+ it 'supports an :only_path context' do
+ doc = reference_filter("Feature Flag #{reference}", only_path: true)
+ link = doc.css('a').first.attr('href')
+
+ expect(link).not_to match %r(https?://)
+ expect(link).to eq urls.edit_project_feature_flag_url(project, feature_flag.iid, only_path: true)
+ end
+ end
+
+ context 'with cross-project / cross-namespace complete reference' do
+ let_it_be(:namespace) { create(:namespace) }
+ let_it_be(:project2) { create(:project, :public, namespace: namespace) }
+ let_it_be(:feature_flag) { create(:operations_feature_flag, project: project2) }
+ let_it_be(:reference) { "[feature_flag:#{project2.full_path}/#{feature_flag.iid}]" }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).to eq urls.edit_project_feature_flag_url(project2, feature_flag)
+ end
+
+ it 'produces a valid text in a link' do
+ doc = reference_filter("See (#{reference}.)")
+
+ expect(doc.css('a').first.text).to eql(reference)
+ end
+
+ it 'produces a valid text' do
+ doc = reference_filter("See (#{reference}.)")
+
+ expect(doc.text).to eql("See (#{reference}.)")
+ end
+
+ it 'ignores invalid feature flag IIDs on the referenced project' do
+ exp = act = "Check [feature_flag:#{non_existing_record_id}]"
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'with cross-project / same-namespace complete reference' do
+ let_it_be(:namespace) { create(:namespace) }
+ let_it_be(:project) { create(:project, :public, namespace: namespace) }
+ let_it_be(:project2) { create(:project, :public, namespace: namespace) }
+ let_it_be(:feature_flag) { create(:operations_feature_flag, project: project2) }
+ let_it_be(:reference) { "[feature_flag:#{project2.full_path}/#{feature_flag.iid}]" }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).to eq urls.edit_project_feature_flag_url(project2, feature_flag)
+ end
+
+ it 'produces a valid text in a link' do
+ doc = reference_filter("See ([feature_flag:#{project2.path}/#{feature_flag.iid}].)")
+
+ expect(doc.css('a').first.text).to eql("[feature_flag:#{project2.path}/#{feature_flag.iid}]")
+ end
+
+ it 'produces a valid text' do
+ doc = reference_filter("See ([feature_flag:#{project2.path}/#{feature_flag.iid}].)")
+
+ expect(doc.text).to eql("See ([feature_flag:#{project2.path}/#{feature_flag.iid}].)")
+ end
+
+ it 'ignores invalid feature flag IIDs on the referenced project' do
+ exp = act = "Check [feature_flag:#{non_existing_record_id}]"
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'with cross-project shorthand reference' do
+ let_it_be(:namespace) { create(:namespace) }
+ let_it_be(:project) { create(:project, :public, namespace: namespace) }
+ let_it_be(:project2) { create(:project, :public, namespace: namespace) }
+ let_it_be(:feature_flag) { create(:operations_feature_flag, project: project2) }
+ let_it_be(:reference) { "[feature_flag:#{project2.path}/#{feature_flag.iid}]" }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).to eq urls.edit_project_feature_flag_url(project2, feature_flag)
+ end
+
+ it 'produces a valid text in a link' do
+ doc = reference_filter("See ([feature_flag:#{project2.path}/#{feature_flag.iid}].)")
+
+ expect(doc.css('a').first.text).to eql("[feature_flag:#{project2.path}/#{feature_flag.iid}]")
+ end
+
+ it 'produces a valid text' do
+ doc = reference_filter("See ([feature_flag:#{project2.path}/#{feature_flag.iid}].)")
+
+ expect(doc.text).to eql("See ([feature_flag:#{project2.path}/#{feature_flag.iid}].)")
+ end
+
+ it 'ignores invalid feature flag IDs on the referenced project' do
+ exp = act = "Check [feature_flag:#{non_existing_record_id}]"
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'with cross-project URL reference' do
+ let_it_be(:namespace) { create(:namespace, name: 'cross-reference') }
+ let_it_be(:project2) { create(:project, :public, namespace: namespace) }
+ let_it_be(:feature_flag) { create(:operations_feature_flag, project: project2) }
+ let_it_be(:reference) { urls.edit_project_feature_flag_url(project2, feature_flag) }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).to eq urls.edit_project_feature_flag_url(project2, feature_flag)
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("See (#{reference}.)")
+
+ expect(doc.to_html).to match(%r{\(<a.+>#{Regexp.escape(feature_flag.to_reference(project))}</a>\.\)})
+ end
+
+ it 'ignores invalid feature flag IIDs on the referenced project' do
+ act = "See #{invalidate_reference(reference)}"
+
+ expect(reference_filter(act).to_html).to match(%r{<a.+>#{Regexp.escape(invalidate_reference(reference))}</a>})
+ end
+ end
+
+ context 'with group context' do
+ let_it_be(:group) { create(:group) }
+
+ it 'links to a valid reference' do
+ reference = "[feature_flag:#{project.full_path}/#{feature_flag.iid}]"
+ result = reference_filter("See #{reference}", { project: nil, group: group } )
+
+ expect(result.css('a').first.attr('href')).to eq(urls.edit_project_feature_flag_url(project, feature_flag))
+ end
+
+ it 'ignores internal references' do
+ exp = act = "See [feature_flag:#{feature_flag.iid}]"
+
+ expect(reference_filter(act, project: nil, group: group).to_html).to eq exp
+ end
+ end
+end
diff --git a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
index df78a3321ba..811c2aca342 100644
--- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
@@ -216,7 +216,7 @@ RSpec.describe Banzai::Filter::MergeRequestReferenceFilter do
end
context 'URL reference for a commit' do
- let(:mr) { create(:merge_request, :with_diffs) }
+ let(:mr) { create(:merge_request) }
let(:reference) do
urls.project_merge_request_url(mr.project, mr) + "/diffs?commit_id=#{mr.diff_head_sha}"
end
diff --git a/spec/lib/banzai/filter/truncate_source_filter_spec.rb b/spec/lib/banzai/filter/truncate_source_filter_spec.rb
index b0c6d91daa8..d5eb8b738b1 100644
--- a/spec/lib/banzai/filter/truncate_source_filter_spec.rb
+++ b/spec/lib/banzai/filter/truncate_source_filter_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe Banzai::Filter::TruncateSourceFilter do
it 'truncates UTF-8 text by bytes, on a character boundary' do
utf8_text = '日本語の文字が大きい'
- truncated = '日…'
+ truncated = '日...'
expect(filter(utf8_text, limit: truncated.bytesize)).to eq(truncated)
expect(filter(utf8_text, limit: utf8_text.bytesize)).to eq(utf8_text)
diff --git a/spec/lib/banzai/pipeline/broadcast_message_pipeline_spec.rb b/spec/lib/banzai/pipeline/broadcast_message_pipeline_spec.rb
index 41a91c56f3b..ad4256c2045 100644
--- a/spec/lib/banzai/pipeline/broadcast_message_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/broadcast_message_pipeline_spec.rb
@@ -3,11 +3,14 @@
require 'spec_helper'
RSpec.describe Banzai::Pipeline::BroadcastMessagePipeline do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+
before do
stub_commonmark_sourcepos_disabled
end
- subject { described_class.to_html(exp, project: spy) }
+ subject { described_class.to_html(exp, project: project) }
context "allows `a` elements" do
let(:exp) { "<a>Link</a>" }
diff --git a/spec/lib/banzai/pipeline/full_pipeline_spec.rb b/spec/lib/banzai/pipeline/full_pipeline_spec.rb
index 9391ca386cf..bcee6f8f65d 100644
--- a/spec/lib/banzai/pipeline/full_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/full_pipeline_spec.rb
@@ -131,4 +131,16 @@ RSpec.describe Banzai::Pipeline::FullPipeline do
expect(output).to include("test [[<em>TOC</em>]]")
end
end
+
+ describe 'backslash escapes' do
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:issue) { create(:issue, project: project) }
+
+ it 'does not convert an escaped reference' do
+ markdown = "\\#{issue.to_reference}"
+ output = described_class.to_html(markdown, project: project)
+
+ expect(output).to include("<span>#</span>#{issue.iid}")
+ end
+ end
end
diff --git a/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb b/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb
new file mode 100644
index 00000000000..241d6db4f11
--- /dev/null
+++ b/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb
@@ -0,0 +1,118 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Banzai::Pipeline::PlainMarkdownPipeline do
+ using RSpec::Parameterized::TableSyntax
+
+ describe 'backslash escapes' do
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:issue) { create(:issue, project: project) }
+
+ def correct_html_included(markdown, expected)
+ result = described_class.call(markdown, {})
+
+ expect(result[:output].to_html).to include(expected)
+
+ result
+ end
+
+ context 'when feature flag honor_escaped_markdown is disabled' do
+ before do
+ stub_feature_flags(honor_escaped_markdown: false)
+ end
+
+ it 'does not escape the markdown' do
+ result = described_class.call(%q(\!), project: project)
+ output = result[:output].to_html
+
+ expect(output).to eq('<p data-sourcepos="1:1-1:2">!</p>')
+ expect(result[:escaped_literals]).to be_falsey
+ end
+ end
+
+ # Test strings taken from https://spec.commonmark.org/0.29/#backslash-escapes
+ describe 'CommonMark tests', :aggregate_failures do
+ it 'converts all ASCII punctuation to literals' do
+ markdown = %q(\!\"\#\$\%\&\'\*\+\,\-\.\/\:\;\<\=\>\?\@\[\]\^\_\`\{\|\}\~) + %q[\(\)\\\\]
+ punctuation = %w(! " # $ % &amp; ' * + , - . / : ; &lt; = &gt; ? @ [ \\ ] ^ _ ` { | } ~) + %w[( )]
+
+ result = described_class.call(markdown, project: project)
+ output = result[:output].to_html
+
+ punctuation.each { |char| expect(output).to include("<span>#{char}</span>") }
+ expect(result[:escaped_literals]).to be_truthy
+ end
+
+ it 'does not convert other characters to literals' do
+ markdown = %q(\→\A\a\ \3\φ\«)
+ expected = '\→\A\a\ \3\φ\«'
+
+ result = correct_html_included(markdown, expected)
+ expect(result[:escaped_literals]).to be_falsey
+ end
+
+ describe 'escaped characters are treated as regular characters and do not have their usual Markdown meanings' do
+ where(:markdown, :expected) do
+ %q(\*not emphasized*) | %q(<span>*</span>not emphasized*)
+ %q(\<br/> not a tag) | %q(<span>&lt;</span>br/&gt; not a tag)
+ %q!\[not a link](/foo)! | %q!<span>[</span>not a link](/foo)!
+ %q(\`not code`) | %q(<span>`</span>not code`)
+ %q(1\. not a list) | %q(1<span>.</span> not a list)
+ %q(\# not a heading) | %q(<span>#</span> not a heading)
+ %q(\[foo]: /url "not a reference") | %q(<span>[</span>foo]: /url "not a reference")
+ %q(\&ouml; not a character entity) | %q(<span>&amp;</span>ouml; not a character entity)
+ end
+
+ with_them do
+ it 'keeps them as literals' do
+ correct_html_included(markdown, expected)
+ end
+ end
+ end
+
+ it 'backslash is itself escaped, the following character is not' do
+ markdown = %q(\\\\*emphasis*)
+ expected = %q(<span>\</span><em>emphasis</em>)
+
+ correct_html_included(markdown, expected)
+ end
+
+ it 'backslash at the end of the line is a hard line break' do
+ markdown = <<~MARKDOWN
+ foo\\
+ bar
+ MARKDOWN
+ expected = "foo<br>\nbar"
+
+ correct_html_included(markdown, expected)
+ end
+
+ describe 'backslash escapes do not work in code blocks, code spans, autolinks, or raw HTML' do
+ where(:markdown, :expected) do
+ %q(`` \[\` ``) | %q(<code>\[\`</code>)
+ %q( \[\]) | %Q(<code>\\[\\]\n</code>)
+ %Q(~~~\n\\[\\]\n~~~) | %Q(<code>\\[\\]\n</code>)
+ %q(<http://example.com?find=\*>) | %q(<a href="http://example.com?find=%5C*">http://example.com?find=\*</a>)
+ %q[<a href="/bar\/)">] | %q[<a href="/bar%5C/)">]
+ end
+
+ with_them do
+ it { correct_html_included(markdown, expected) }
+ end
+ end
+
+ describe 'work in all other contexts, including URLs and link titles, link references, and info strings in fenced code blocks' do
+ where(:markdown, :expected) do
+ %q![foo](/bar\* "ti\*tle")! | %q(<a href="/bar*" title="ti*tle">foo</a>)
+ %Q![foo]\n\n[foo]: /bar\\* "ti\\*tle"! | %q(<a href="/bar*" title="ti*tle">foo</a>)
+ %Q(``` foo\\+bar\nfoo\n```) | %Q(<code lang="foo+bar">foo\n</code>)
+ end
+
+ with_them do
+ it { correct_html_included(markdown, expected) }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/banzai/pipeline/pre_process_pipeline_spec.rb b/spec/lib/banzai/pipeline/pre_process_pipeline_spec.rb
index f0498f41b61..c628d8d5b41 100644
--- a/spec/lib/banzai/pipeline/pre_process_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/pre_process_pipeline_spec.rb
@@ -30,6 +30,6 @@ RSpec.describe Banzai::Pipeline::PreProcessPipeline do
result = described_class.call(text, limit: 12)
- expect(result[:output]).to eq('foo foo f…')
+ expect(result[:output]).to eq('foo foo f...')
end
end
diff --git a/spec/lib/banzai/reference_parser/feature_flag_parser_spec.rb b/spec/lib/banzai/reference_parser/feature_flag_parser_spec.rb
new file mode 100644
index 00000000000..288eb9ae360
--- /dev/null
+++ b/spec/lib/banzai/reference_parser/feature_flag_parser_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Banzai::ReferenceParser::FeatureFlagParser do
+ include ReferenceParserHelpers
+
+ subject { described_class.new(Banzai::RenderContext.new(project, user)) }
+
+ let(:link) { empty_html_link }
+
+ describe '#nodes_visible_to_user' do
+ let(:project) { create(:project, :public) }
+ let(:user) { create(:user) }
+ let(:feature_flag) { create(:operations_feature_flag, project: project) }
+
+ context 'when the link has a data-issue attribute' do
+ before do
+ link['data-feature-flag'] = feature_flag.id.to_s
+ end
+
+ it_behaves_like "referenced feature visibility", "issues", "merge_requests" do
+ before do
+ project.add_developer(user) if enable_user?
+ end
+ end
+ end
+ end
+
+ describe '#referenced_by' do
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:feature_flag) { create(:operations_feature_flag, project: project) }
+
+ describe 'when the link has a data-feature-flag attribute' do
+ context 'using an existing feature flag ID' do
+ it 'returns an Array of feature flags' do
+ link['data-feature-flag'] = feature_flag.id.to_s
+
+ expect(subject.referenced_by([link])).to eq([feature_flag])
+ end
+ end
+
+ context 'using a non-existing feature flag ID' do
+ it 'returns an empty Array' do
+ link['data-feature-flag'] = ''
+
+ expect(subject.referenced_by([link])).to eq([])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/bulk_imports/common/extractors/graphql_extractor_spec.rb b/spec/lib/bulk_imports/common/extractors/graphql_extractor_spec.rb
index 2abd3df20fd..80607485b6e 100644
--- a/spec/lib/bulk_imports/common/extractors/graphql_extractor_spec.rb
+++ b/spec/lib/bulk_imports/common/extractors/graphql_extractor_spec.rb
@@ -5,8 +5,18 @@ require 'spec_helper'
RSpec.describe BulkImports::Common::Extractors::GraphqlExtractor do
let(:graphql_client) { instance_double(BulkImports::Clients::Graphql) }
let(:import_entity) { create(:bulk_import_entity) }
- let(:response) { double(original_hash: { foo: :bar }) }
- let(:query) { { query: double(to_s: 'test', variables: {}) } }
+ let(:response) { double(original_hash: { 'data' => { 'foo' => 'bar' }, 'page_info' => {} }) }
+ let(:options) do
+ {
+ query: double(
+ to_s: 'test',
+ variables: {},
+ data_path: %w[data foo],
+ page_info_path: %w[data page_info]
+ )
+ }
+ end
+
let(:context) do
instance_double(
BulkImports::Pipeline::Context,
@@ -14,58 +24,20 @@ RSpec.describe BulkImports::Common::Extractors::GraphqlExtractor do
)
end
- subject { described_class.new(query) }
-
- before do
- allow(subject).to receive(:graphql_client).and_return(graphql_client)
- allow(graphql_client).to receive(:parse)
- end
+ subject { described_class.new(options) }
describe '#extract' do
before do
- allow(subject).to receive(:query_variables).and_return({})
- allow(graphql_client).to receive(:execute).and_return(response)
- end
-
- it 'returns original hash' do
- expect(subject.extract(context)).to eq({ foo: :bar })
- end
- end
-
- describe 'query variables' do
- before do
+ allow(subject).to receive(:graphql_client).and_return(graphql_client)
+ allow(graphql_client).to receive(:parse)
allow(graphql_client).to receive(:execute).and_return(response)
end
- context 'when variables are present' do
- let(:variables) { { foo: :bar } }
- let(:query) { { query: double(to_s: 'test', variables: variables) } }
-
- it 'builds graphql query variables for import entity' do
- expect(graphql_client).to receive(:execute).with(anything, variables)
-
- subject.extract(context).first
- end
- end
-
- context 'when no variables are present' do
- let(:query) { { query: double(to_s: 'test', variables: nil) } }
-
- it 'returns empty hash' do
- expect(graphql_client).to receive(:execute).with(anything, nil)
-
- subject.extract(context).first
- end
- end
-
- context 'when variables are empty hash' do
- let(:query) { { query: double(to_s: 'test', variables: {}) } }
-
- it 'makes graphql request with empty hash' do
- expect(graphql_client).to receive(:execute).with(anything, {})
+ it 'returns ExtractedData' do
+ extracted_data = subject.extract(context)
- subject.extract(context).first
- end
+ expect(extracted_data).to be_instance_of(BulkImports::Pipeline::ExtractedData)
+ expect(extracted_data.data).to contain_exactly('bar')
end
end
end
diff --git a/spec/lib/bulk_imports/common/loaders/entity_loader_spec.rb b/spec/lib/bulk_imports/common/loaders/entity_loader_spec.rb
index 4de7d95172f..57ffdfa9aee 100644
--- a/spec/lib/bulk_imports/common/loaders/entity_loader_spec.rb
+++ b/spec/lib/bulk_imports/common/loaders/entity_loader_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe BulkImports::Common::Loaders::EntityLoader do
it "creates entities for the given data" do
group = create(:group, path: "imported-group")
parent_entity = create(:bulk_import_entity, group: group, bulk_import: create(:bulk_import))
- context = instance_double(BulkImports::Pipeline::Context, entity: parent_entity)
+ context = BulkImports::Pipeline::Context.new(parent_entity)
data = {
source_type: :group_entity,
diff --git a/spec/lib/bulk_imports/common/transformers/award_emoji_transformer_spec.rb b/spec/lib/bulk_imports/common/transformers/award_emoji_transformer_spec.rb
new file mode 100644
index 00000000000..5b560a30bf5
--- /dev/null
+++ b/spec/lib/bulk_imports/common/transformers/award_emoji_transformer_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Common::Transformers::AwardEmojiTransformer do
+ describe '#transform' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:bulk_import) { create(:bulk_import) }
+ let_it_be(:entity) { create(:bulk_import_entity, bulk_import: bulk_import, group: group) }
+ let_it_be(:context) { BulkImports::Pipeline::Context.new(entity) }
+
+ let(:hash) do
+ {
+ 'name' => 'thumbs up',
+ 'user' => {
+ 'public_email' => email
+ }
+ }
+ end
+
+ before do
+ group.add_developer(user)
+ end
+
+ shared_examples 'sets user_id and removes user key' do
+ it 'sets found user_id and removes user key' do
+ transformed_hash = subject.transform(context, hash)
+
+ expect(transformed_hash['user']).to be_nil
+ expect(transformed_hash['user_id']).to eq(user.id)
+ end
+ end
+
+ context 'when user can be found by email' do
+ let(:email) { user.email }
+
+ include_examples 'sets user_id and removes user key'
+ end
+
+ context 'when user cannot be found by email' do
+ let(:user) { bulk_import.user }
+ let(:email) { nil }
+
+ include_examples 'sets user_id and removes user key'
+ end
+ end
+end
diff --git a/spec/lib/bulk_imports/common/transformers/hash_key_digger_spec.rb b/spec/lib/bulk_imports/common/transformers/hash_key_digger_spec.rb
deleted file mode 100644
index 2b33701653e..00000000000
--- a/spec/lib/bulk_imports/common/transformers/hash_key_digger_spec.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe BulkImports::Common::Transformers::HashKeyDigger do
- describe '#transform' do
- it 'when the key_path is an array' do
- data = { foo: { bar: :value } }
- key_path = %i[foo bar]
- transformed = described_class.new(key_path: key_path).transform(nil, data)
-
- expect(transformed).to eq(:value)
- end
-
- it 'when the key_path is not an array' do
- data = { foo: { bar: :value } }
- key_path = :foo
- transformed = described_class.new(key_path: key_path).transform(nil, data)
-
- expect(transformed).to eq({ bar: :value })
- end
-
- it "when the data is not a hash" do
- expect { described_class.new(key_path: nil).transform(nil, nil) }
- .to raise_error(ArgumentError, "Given data must be a Hash")
- end
- end
-end
diff --git a/spec/lib/bulk_imports/common/transformers/underscorify_keys_transformer_spec.rb b/spec/lib/bulk_imports/common/transformers/underscorify_keys_transformer_spec.rb
deleted file mode 100644
index cdffa750694..00000000000
--- a/spec/lib/bulk_imports/common/transformers/underscorify_keys_transformer_spec.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe BulkImports::Common::Transformers::UnderscorifyKeysTransformer do
- describe '#transform' do
- it 'deep underscorifies hash keys' do
- data = {
- 'fullPath' => 'Foo',
- 'snakeKeys' => {
- 'snakeCaseKey' => 'Bar',
- 'moreKeys' => {
- 'anotherSnakeCaseKey' => 'Test'
- }
- }
- }
-
- transformed_data = described_class.new.transform(nil, data)
-
- expect(transformed_data).to have_key('full_path')
- expect(transformed_data).to have_key('snake_keys')
- expect(transformed_data['snake_keys']).to have_key('snake_case_key')
- expect(transformed_data['snake_keys']).to have_key('more_keys')
- expect(transformed_data.dig('snake_keys', 'more_keys')).to have_key('another_snake_case_key')
- end
- end
-end
diff --git a/spec/lib/bulk_imports/groups/extractors/subgroups_extractor_spec.rb b/spec/lib/bulk_imports/groups/extractors/subgroups_extractor_spec.rb
new file mode 100644
index 00000000000..627247c04ab
--- /dev/null
+++ b/spec/lib/bulk_imports/groups/extractors/subgroups_extractor_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Groups::Extractors::SubgroupsExtractor do
+ describe '#extract' do
+ it 'returns ExtractedData response' do
+ bulk_import = create(:bulk_import)
+ create(:bulk_import_configuration, bulk_import: bulk_import)
+ entity = create(:bulk_import_entity, bulk_import: bulk_import)
+ response = [{ 'test' => 'group' }]
+ context = BulkImports::Pipeline::Context.new(entity)
+
+ allow_next_instance_of(BulkImports::Clients::Http) do |client|
+ allow(client).to receive(:each_page).and_return(response)
+ end
+
+ extracted_data = subject.extract(context)
+
+ expect(extracted_data).to be_instance_of(BulkImports::Pipeline::ExtractedData)
+ expect(extracted_data.data).to eq(response)
+ end
+ end
+end
diff --git a/spec/lib/bulk_imports/groups/graphql/get_group_query_spec.rb b/spec/lib/bulk_imports/groups/graphql/get_group_query_spec.rb
new file mode 100644
index 00000000000..ef46da7062b
--- /dev/null
+++ b/spec/lib/bulk_imports/groups/graphql/get_group_query_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Groups::Graphql::GetGroupQuery do
+ describe '#variables' do
+ let(:entity) { double(source_full_path: 'test', bulk_import: nil) }
+ let(:context) { BulkImports::Pipeline::Context.new(entity) }
+
+ it 'returns query variables based on entity information' do
+ expected = { full_path: entity.source_full_path }
+
+ expect(described_class.variables(context)).to eq(expected)
+ end
+ end
+
+ describe '#data_path' do
+ it 'returns data path' do
+ expected = %w[data group]
+
+ expect(described_class.data_path).to eq(expected)
+ end
+ end
+
+ describe '#page_info_path' do
+ it 'returns pagination information path' do
+ expected = %w[data group page_info]
+
+ expect(described_class.page_info_path).to eq(expected)
+ end
+ end
+end
diff --git a/spec/lib/bulk_imports/groups/graphql/get_labels_query_spec.rb b/spec/lib/bulk_imports/groups/graphql/get_labels_query_spec.rb
new file mode 100644
index 00000000000..247da200d68
--- /dev/null
+++ b/spec/lib/bulk_imports/groups/graphql/get_labels_query_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Groups::Graphql::GetLabelsQuery do
+ describe '#variables' do
+ let(:entity) { double(source_full_path: 'test', next_page_for: 'next_page', bulk_import: nil) }
+ let(:context) { BulkImports::Pipeline::Context.new(entity) }
+
+ it 'returns query variables based on entity information' do
+ expected = { full_path: entity.source_full_path, cursor: entity.next_page_for }
+
+ expect(described_class.variables(context)).to eq(expected)
+ end
+ end
+
+ describe '#data_path' do
+ it 'returns data path' do
+ expected = %w[data group labels nodes]
+
+ expect(described_class.data_path).to eq(expected)
+ end
+ end
+
+ describe '#page_info_path' do
+ it 'returns pagination information path' do
+ expected = %w[data group labels page_info]
+
+ expect(described_class.page_info_path).to eq(expected)
+ end
+ end
+end
diff --git a/spec/lib/bulk_imports/groups/graphql/get_members_query_spec.rb b/spec/lib/bulk_imports/groups/graphql/get_members_query_spec.rb
new file mode 100644
index 00000000000..5d05f5a2d30
--- /dev/null
+++ b/spec/lib/bulk_imports/groups/graphql/get_members_query_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Groups::Graphql::GetMembersQuery do
+ it 'has a valid query' do
+ entity = create(:bulk_import_entity)
+ context = BulkImports::Pipeline::Context.new(entity)
+
+ query = GraphQL::Query.new(
+ GitlabSchema,
+ described_class.to_s,
+ variables: described_class.variables(context)
+ )
+ result = GitlabSchema.static_validator.validate(query)
+
+ expect(result[:errors]).to be_empty
+ end
+
+ describe '#data_path' do
+ it 'returns data path' do
+ expected = %w[data group group_members nodes]
+
+ expect(described_class.data_path).to eq(expected)
+ end
+ end
+
+ describe '#page_info_path' do
+ it 'returns pagination information path' do
+ expected = %w[data group group_members page_info]
+
+ expect(described_class.page_info_path).to eq(expected)
+ end
+ end
+end
diff --git a/spec/lib/bulk_imports/groups/loaders/group_loader_spec.rb b/spec/lib/bulk_imports/groups/loaders/group_loader_spec.rb
index b14dfc615a9..183292722d2 100644
--- a/spec/lib/bulk_imports/groups/loaders/group_loader_spec.rb
+++ b/spec/lib/bulk_imports/groups/loaders/group_loader_spec.rb
@@ -7,21 +7,20 @@ RSpec.describe BulkImports::Groups::Loaders::GroupLoader do
let(:user) { create(:user) }
let(:data) { { foo: :bar } }
let(:service_double) { instance_double(::Groups::CreateService) }
- let(:entity) { create(:bulk_import_entity) }
- let(:context) do
- instance_double(
- BulkImports::Pipeline::Context,
- entity: entity,
- current_user: user
- )
- end
+ let(:bulk_import) { create(:bulk_import, user: user) }
+ let(:entity) { create(:bulk_import_entity, bulk_import: bulk_import) }
+ let(:context) { BulkImports::Pipeline::Context.new(entity) }
subject { described_class.new }
context 'when user can create group' do
shared_examples 'calls Group Create Service to create a new group' do
it 'calls Group Create Service to create a new group' do
- expect(::Groups::CreateService).to receive(:new).with(context.current_user, data).and_return(service_double)
+ expect(::Groups::CreateService)
+ .to receive(:new)
+ .with(context.current_user, data)
+ .and_return(service_double)
+
expect(service_double).to receive(:execute)
expect(entity).to receive(:update!)
diff --git a/spec/lib/bulk_imports/groups/loaders/labels_loader_spec.rb b/spec/lib/bulk_imports/groups/loaders/labels_loader_spec.rb
new file mode 100644
index 00000000000..ac2f9c8cb1d
--- /dev/null
+++ b/spec/lib/bulk_imports/groups/loaders/labels_loader_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Groups::Loaders::LabelsLoader do
+ describe '#load' do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:entity) { create(:bulk_import_entity, group: group) }
+ let(:context) { BulkImports::Pipeline::Context.new(entity) }
+
+ let(:data) do
+ {
+ 'title' => 'label',
+ 'description' => 'description',
+ 'color' => '#FFFFFF'
+ }
+ end
+
+ it 'creates the label' do
+ expect { subject.load(context, data) }.to change(Label, :count).by(1)
+
+ label = group.labels.first
+
+ expect(label.title).to eq(data['title'])
+ expect(label.description).to eq(data['description'])
+ expect(label.color).to eq(data['color'])
+ end
+ end
+end
diff --git a/spec/lib/bulk_imports/groups/loaders/members_loader_spec.rb b/spec/lib/bulk_imports/groups/loaders/members_loader_spec.rb
new file mode 100644
index 00000000000..d552578e7be
--- /dev/null
+++ b/spec/lib/bulk_imports/groups/loaders/members_loader_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Groups::Loaders::MembersLoader do
+ describe '#load' do
+ let_it_be(:user_importer) { create(:user) }
+ let_it_be(:user_member) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:bulk_import) { create(:bulk_import, user: user_importer) }
+ let_it_be(:entity) { create(:bulk_import_entity, bulk_import: bulk_import, group: group) }
+ let_it_be(:context) { BulkImports::Pipeline::Context.new(entity) }
+
+ let_it_be(:data) do
+ {
+ 'user_id' => user_member.id,
+ 'created_by_id' => user_importer.id,
+ 'access_level' => 30,
+ 'created_at' => '2020-01-01T00:00:00Z',
+ 'updated_at' => '2020-01-01T00:00:00Z',
+ 'expires_at' => nil
+ }
+ end
+
+ it 'does nothing when there is no data' do
+ expect { subject.load(context, nil) }.not_to change(GroupMember, :count)
+ end
+
+ it 'creates the member' do
+ expect { subject.load(context, data) }.to change(GroupMember, :count).by(1)
+
+ member = group.members.last
+
+ expect(member.user).to eq(user_member)
+ expect(member.created_by).to eq(user_importer)
+ expect(member.access_level).to eq(30)
+ expect(member.created_at).to eq('2020-01-01T00:00:00Z')
+ expect(member.updated_at).to eq('2020-01-01T00:00:00Z')
+ expect(member.expires_at).to eq(nil)
+ end
+ end
+end
diff --git a/spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb
index 1a91f3d7a78..61950cdd9b0 100644
--- a/spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb
@@ -6,41 +6,34 @@ RSpec.describe BulkImports::Groups::Pipelines::GroupPipeline do
describe '#run' do
let(:user) { create(:user) }
let(:parent) { create(:group) }
+ let(:bulk_import) { create(:bulk_import, user: user) }
let(:entity) do
create(
:bulk_import_entity,
+ bulk_import: bulk_import,
source_full_path: 'source/full/path',
destination_name: 'My Destination Group',
destination_namespace: parent.full_path
)
end
- let(:context) do
- BulkImports::Pipeline::Context.new(
- current_user: user,
- entity: entity
- )
- end
+ let(:context) { BulkImports::Pipeline::Context.new(entity) }
let(:group_data) do
{
- 'data' => {
- 'group' => {
- 'name' => 'source_name',
- 'fullPath' => 'source/full/path',
- 'visibility' => 'private',
- 'projectCreationLevel' => 'developer',
- 'subgroupCreationLevel' => 'maintainer',
- 'description' => 'Group Description',
- 'emailsDisabled' => true,
- 'lfsEnabled' => false,
- 'mentionsDisabled' => true
- }
- }
+ 'name' => 'source_name',
+ 'full_path' => 'source/full/path',
+ 'visibility' => 'private',
+ 'project_creation_level' => 'developer',
+ 'subgroup_creation_level' => 'maintainer',
+ 'description' => 'Group Description',
+ 'emails_disabled' => true,
+ 'lfs_enabled' => false,
+ 'mentions_disabled' => true
}
end
- subject { described_class.new }
+ subject { described_class.new(context) }
before do
allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor|
@@ -53,20 +46,20 @@ RSpec.describe BulkImports::Groups::Pipelines::GroupPipeline do
it 'imports new group into destination group' do
group_path = 'my-destination-group'
- subject.run(context)
+ subject.run
imported_group = Group.find_by_path(group_path)
expect(imported_group).not_to be_nil
expect(imported_group.parent).to eq(parent)
expect(imported_group.path).to eq(group_path)
- expect(imported_group.description).to eq(group_data.dig('data', 'group', 'description'))
- expect(imported_group.visibility).to eq(group_data.dig('data', 'group', 'visibility'))
- expect(imported_group.project_creation_level).to eq(Gitlab::Access.project_creation_string_options[group_data.dig('data', 'group', 'projectCreationLevel')])
- expect(imported_group.subgroup_creation_level).to eq(Gitlab::Access.subgroup_creation_string_options[group_data.dig('data', 'group', 'subgroupCreationLevel')])
- expect(imported_group.lfs_enabled?).to eq(group_data.dig('data', 'group', 'lfsEnabled'))
- expect(imported_group.emails_disabled?).to eq(group_data.dig('data', 'group', 'emailsDisabled'))
- expect(imported_group.mentions_disabled?).to eq(group_data.dig('data', 'group', 'mentionsDisabled'))
+ expect(imported_group.description).to eq(group_data['description'])
+ expect(imported_group.visibility).to eq(group_data['visibility'])
+ expect(imported_group.project_creation_level).to eq(Gitlab::Access.project_creation_string_options[group_data['project_creation_level']])
+ expect(imported_group.subgroup_creation_level).to eq(Gitlab::Access.subgroup_creation_string_options[group_data['subgroup_creation_level']])
+ expect(imported_group.lfs_enabled?).to eq(group_data['lfs_enabled'])
+ expect(imported_group.emails_disabled?).to eq(group_data['emails_disabled'])
+ expect(imported_group.mentions_disabled?).to eq(group_data['mentions_disabled'])
end
end
@@ -87,8 +80,6 @@ RSpec.describe BulkImports::Groups::Pipelines::GroupPipeline do
it 'has transformers' do
expect(described_class.transformers)
.to contain_exactly(
- { klass: BulkImports::Common::Transformers::HashKeyDigger, options: { key_path: %w[data group] } },
- { klass: BulkImports::Common::Transformers::UnderscorifyKeysTransformer, options: nil },
{ klass: BulkImports::Common::Transformers::ProhibitedAttributesTransformer, options: nil },
{ klass: BulkImports::Groups::Transformers::GroupAttributesTransformer, options: nil }
)
diff --git a/spec/lib/bulk_imports/groups/pipelines/labels_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/labels_pipeline_spec.rb
new file mode 100644
index 00000000000..63f28916d9a
--- /dev/null
+++ b/spec/lib/bulk_imports/groups/pipelines/labels_pipeline_spec.rb
@@ -0,0 +1,118 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Groups::Pipelines::LabelsPipeline do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:cursor) { 'cursor' }
+ let(:entity) do
+ create(
+ :bulk_import_entity,
+ source_full_path: 'source/full/path',
+ destination_name: 'My Destination Group',
+ destination_namespace: group.full_path,
+ group: group
+ )
+ end
+
+ let(:context) { BulkImports::Pipeline::Context.new(entity) }
+
+ subject { described_class.new(context) }
+
+ def extractor_data(title:, has_next_page:, cursor: nil)
+ data = [
+ {
+ 'title' => title,
+ 'description' => 'desc',
+ 'color' => '#428BCA'
+ }
+ ]
+
+ page_info = {
+ 'end_cursor' => cursor,
+ 'has_next_page' => has_next_page
+ }
+
+ BulkImports::Pipeline::ExtractedData.new(data: data, page_info: page_info)
+ end
+
+ describe '#run' do
+ it 'imports a group labels' do
+ first_page = extractor_data(title: 'label1', has_next_page: true, cursor: cursor)
+ last_page = extractor_data(title: 'label2', has_next_page: false)
+
+ allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor|
+ allow(extractor)
+ .to receive(:extract)
+ .and_return(first_page, last_page)
+ end
+
+ expect { subject.run }.to change(Label, :count).by(2)
+
+ label = group.labels.order(:created_at).last
+
+ expect(label.title).to eq('label2')
+ expect(label.description).to eq('desc')
+ expect(label.color).to eq('#428BCA')
+ end
+ end
+
+ describe '#after_run' do
+ context 'when extracted data has next page' do
+ it 'updates tracker information and runs pipeline again' do
+ data = extractor_data(title: 'label', has_next_page: true, cursor: cursor)
+
+ expect(subject).to receive(:run)
+
+ subject.after_run(data)
+
+ tracker = entity.trackers.find_by(relation: :labels)
+
+ expect(tracker.has_next_page).to eq(true)
+ expect(tracker.next_page).to eq(cursor)
+ end
+ end
+
+ context 'when extracted data has no next page' do
+ it 'updates tracker information and does not run pipeline' do
+ data = extractor_data(title: 'label', has_next_page: false)
+
+ expect(subject).not_to receive(:run)
+
+ subject.after_run(data)
+
+ tracker = entity.trackers.find_by(relation: :labels)
+
+ expect(tracker.has_next_page).to eq(false)
+ expect(tracker.next_page).to be_nil
+ end
+ end
+ end
+
+ describe 'pipeline parts' do
+ it { expect(described_class).to include_module(BulkImports::Pipeline) }
+ it { expect(described_class).to include_module(BulkImports::Pipeline::Runner) }
+
+ it 'has extractors' do
+ expect(described_class.get_extractor)
+ .to eq(
+ klass: BulkImports::Common::Extractors::GraphqlExtractor,
+ options: {
+ query: BulkImports::Groups::Graphql::GetLabelsQuery
+ }
+ )
+ end
+
+ it 'has transformers' do
+ expect(described_class.transformers)
+ .to contain_exactly(
+ { klass: BulkImports::Common::Transformers::ProhibitedAttributesTransformer, options: nil }
+ )
+ end
+
+ it 'has loaders' do
+ expect(described_class.get_loader).to eq(klass: BulkImports::Groups::Loaders::LabelsLoader, options: nil)
+ end
+ end
+end
diff --git a/spec/lib/bulk_imports/groups/pipelines/members_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/members_pipeline_spec.rb
new file mode 100644
index 00000000000..9f498f8154f
--- /dev/null
+++ b/spec/lib/bulk_imports/groups/pipelines/members_pipeline_spec.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Groups::Pipelines::MembersPipeline do
+ let_it_be(:member_user1) { create(:user, email: 'email1@email.com') }
+ let_it_be(:member_user2) { create(:user, email: 'email2@email.com') }
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:cursor) { 'cursor' }
+ let_it_be(:bulk_import) { create(:bulk_import, user: user) }
+ let_it_be(:entity) { create(:bulk_import_entity, bulk_import: bulk_import, group: group) }
+ let_it_be(:context) { BulkImports::Pipeline::Context.new(entity) }
+
+ subject { described_class.new(context) }
+
+ describe '#run' do
+ it 'maps existing users to the imported group' do
+ first_page = member_data(email: member_user1.email, has_next_page: true, cursor: cursor)
+ last_page = member_data(email: member_user2.email, has_next_page: false)
+
+ allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor|
+ allow(extractor)
+ .to receive(:extract)
+ .and_return(first_page, last_page)
+ end
+
+ expect { subject.run }.to change(GroupMember, :count).by(2)
+
+ members = group.members.map { |m| m.slice(:user_id, :access_level) }
+
+ expect(members).to contain_exactly(
+ { user_id: member_user1.id, access_level: 30 },
+ { user_id: member_user2.id, access_level: 30 }
+ )
+ end
+ end
+
+ describe 'pipeline parts' do
+ it { expect(described_class).to include_module(BulkImports::Pipeline) }
+ it { expect(described_class).to include_module(BulkImports::Pipeline::Runner) }
+
+ it 'has extractors' do
+ expect(described_class.get_extractor)
+ .to eq(
+ klass: BulkImports::Common::Extractors::GraphqlExtractor,
+ options: {
+ query: BulkImports::Groups::Graphql::GetMembersQuery
+ }
+ )
+ end
+
+ it 'has transformers' do
+ expect(described_class.transformers)
+ .to contain_exactly(
+ { klass: BulkImports::Common::Transformers::ProhibitedAttributesTransformer, options: nil },
+ { klass: BulkImports::Groups::Transformers::MemberAttributesTransformer, options: nil }
+ )
+ end
+
+ it 'has loaders' do
+ expect(described_class.get_loader).to eq(klass: BulkImports::Groups::Loaders::MembersLoader, options: nil)
+ end
+ end
+
+ def member_data(email:, has_next_page:, cursor: nil)
+ data = {
+ 'created_at' => '2020-01-01T00:00:00Z',
+ 'updated_at' => '2020-01-01T00:00:00Z',
+ 'expires_at' => nil,
+ 'access_level' => {
+ 'integer_value' => 30
+ },
+ 'user' => {
+ 'public_email' => email
+ }
+ }
+
+ page_info = {
+ 'end_cursor' => cursor,
+ 'has_next_page' => has_next_page
+ }
+
+ BulkImports::Pipeline::ExtractedData.new(data: data, page_info: page_info)
+ end
+end
diff --git a/spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb
index e5a8ed7f47d..0404c52b895 100644
--- a/spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb
@@ -14,13 +14,7 @@ RSpec.describe BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline do
)
end
- let(:context) do
- instance_double(
- BulkImports::Pipeline::Context,
- current_user: user,
- entity: parent_entity
- )
- end
+ let(:context) { BulkImports::Pipeline::Context.new(parent_entity) }
let(:subgroup_data) do
[
@@ -31,7 +25,7 @@ RSpec.describe BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline do
]
end
- subject { described_class.new }
+ subject { described_class.new(context) }
before do
allow_next_instance_of(BulkImports::Groups::Extractors::SubgroupsExtractor) do |extractor|
@@ -42,7 +36,7 @@ RSpec.describe BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline do
end
it 'creates entities for the subgroups' do
- expect { subject.run(context) }.to change(BulkImports::Entity, :count).by(1)
+ expect { subject.run }.to change(BulkImports::Entity, :count).by(1)
subgroup_entity = BulkImports::Entity.last
diff --git a/spec/lib/bulk_imports/groups/transformers/group_attributes_transformer_spec.rb b/spec/lib/bulk_imports/groups/transformers/group_attributes_transformer_spec.rb
index 28a7859915d..5a7a51675d6 100644
--- a/spec/lib/bulk_imports/groups/transformers/group_attributes_transformer_spec.rb
+++ b/spec/lib/bulk_imports/groups/transformers/group_attributes_transformer_spec.rb
@@ -7,22 +7,18 @@ RSpec.describe BulkImports::Groups::Transformers::GroupAttributesTransformer do
let(:user) { create(:user) }
let(:parent) { create(:group) }
let(:group) { create(:group, name: 'My Source Group', parent: parent) }
+ let(:bulk_import) { create(:bulk_import, user: user) }
let(:entity) do
- instance_double(
- BulkImports::Entity,
+ create(
+ :bulk_import_entity,
+ bulk_import: bulk_import,
source_full_path: 'source/full/path',
destination_name: group.name,
destination_namespace: parent.full_path
)
end
- let(:context) do
- instance_double(
- BulkImports::Pipeline::Context,
- current_user: user,
- entity: entity
- )
- end
+ let(:context) { BulkImports::Pipeline::Context.new(entity) }
let(:data) do
{
@@ -85,16 +81,16 @@ RSpec.describe BulkImports::Groups::Transformers::GroupAttributesTransformer do
end
context 'when destination namespace is user namespace' do
- let(:entity) do
- instance_double(
- BulkImports::Entity,
+ it 'does not set parent id' do
+ entity = create(
+ :bulk_import_entity,
+ bulk_import: bulk_import,
source_full_path: 'source/full/path',
destination_name: group.name,
destination_namespace: user.namespace.full_path
)
- end
+ context = BulkImports::Pipeline::Context.new(entity)
- it 'does not set parent id' do
transformed_data = subject.transform(context, data)
expect(transformed_data).not_to have_key('parent_id')
diff --git a/spec/lib/bulk_imports/groups/transformers/member_attributes_transformer_spec.rb b/spec/lib/bulk_imports/groups/transformers/member_attributes_transformer_spec.rb
new file mode 100644
index 00000000000..f66c67fc6a2
--- /dev/null
+++ b/spec/lib/bulk_imports/groups/transformers/member_attributes_transformer_spec.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Groups::Transformers::MemberAttributesTransformer do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:secondary_email) { 'secondary@email.com' }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:bulk_import) { create(:bulk_import, user: user) }
+ let_it_be(:entity) { create(:bulk_import_entity, bulk_import: bulk_import, group: group) }
+ let_it_be(:context) { BulkImports::Pipeline::Context.new(entity) }
+
+ it 'returns nil when receives no data' do
+ expect(subject.transform(context, nil)).to eq(nil)
+ end
+
+ it 'returns nil when no user is found' do
+ expect(subject.transform(context, member_data)).to eq(nil)
+ expect(subject.transform(context, member_data(email: 'inexistent@email.com'))).to eq(nil)
+ end
+
+ context 'when the user is not confirmed' do
+ before do
+ user.update!(confirmed_at: nil)
+ end
+
+ it 'returns nil even when the primary email match' do
+ data = member_data(email: user.email)
+
+ expect(subject.transform(context, data)).to eq(nil)
+ end
+
+ it 'returns nil even when a secondary email match' do
+ user.emails << Email.new(email: secondary_email)
+ data = member_data(email: secondary_email)
+
+ expect(subject.transform(context, data)).to eq(nil)
+ end
+ end
+
+ context 'when the user is confirmed' do
+ before do
+ user.update!(confirmed_at: Time.now.utc)
+ end
+
+ it 'finds the user by the primary email' do
+ data = member_data(email: user.email)
+
+ expect(subject.transform(context, data)).to eq(
+ 'access_level' => 30,
+ 'user_id' => user.id,
+ 'created_by_id' => user.id,
+ 'created_at' => '2020-01-01T00:00:00Z',
+ 'updated_at' => '2020-01-01T00:00:00Z',
+ 'expires_at' => nil
+ )
+ end
+
+ it 'finds the user by the secondary email' do
+ user.emails << Email.new(email: secondary_email, confirmed_at: Time.now.utc)
+ data = member_data(email: secondary_email)
+
+ expect(subject.transform(context, data)).to eq(
+ 'access_level' => 30,
+ 'user_id' => user.id,
+ 'created_by_id' => user.id,
+ 'created_at' => '2020-01-01T00:00:00Z',
+ 'updated_at' => '2020-01-01T00:00:00Z',
+ 'expires_at' => nil
+ )
+ end
+
+ context 'format access level' do
+ it 'ignores record if no access level is given' do
+ data = member_data(email: user.email, access_level: nil)
+
+ expect(subject.transform(context, data)).to be_nil
+ end
+
+ it 'ignores record if is not a valid access level' do
+ data = member_data(email: user.email, access_level: 999)
+
+ expect(subject.transform(context, data)).to be_nil
+ end
+ end
+ end
+
+ def member_data(email: '', access_level: 30)
+ {
+ 'created_at' => '2020-01-01T00:00:00Z',
+ 'updated_at' => '2020-01-01T00:00:00Z',
+ 'expires_at' => nil,
+ 'access_level' => {
+ 'integer_value' => access_level
+ },
+ 'user' => {
+ 'public_email' => email
+ }
+ }
+ end
+end
diff --git a/spec/lib/bulk_imports/importers/group_importer_spec.rb b/spec/lib/bulk_imports/importers/group_importer_spec.rb
index 87baf1b8026..b4fdb7b5e5b 100644
--- a/spec/lib/bulk_imports/importers/group_importer_spec.rb
+++ b/spec/lib/bulk_imports/importers/group_importer_spec.rb
@@ -4,28 +4,29 @@ require 'spec_helper'
RSpec.describe BulkImports::Importers::GroupImporter do
let(:user) { create(:user) }
+ let(:group) { create(:group) }
let(:bulk_import) { create(:bulk_import) }
- let(:bulk_import_entity) { create(:bulk_import_entity, :started, bulk_import: bulk_import) }
+ let(:bulk_import_entity) { create(:bulk_import_entity, :started, bulk_import: bulk_import, group: group) }
let(:bulk_import_configuration) { create(:bulk_import_configuration, bulk_import: bulk_import) }
- let(:context) do
- BulkImports::Pipeline::Context.new(
- current_user: user,
- entity: bulk_import_entity,
- configuration: bulk_import_configuration
- )
- end
-
- subject { described_class.new(bulk_import_entity) }
+ let(:context) { BulkImports::Pipeline::Context.new(bulk_import_entity) }
before do
allow(BulkImports::Pipeline::Context).to receive(:new).and_return(context)
end
+ subject { described_class.new(bulk_import_entity) }
+
describe '#execute' do
it 'starts the entity and run its pipelines' do
expect_to_run_pipeline BulkImports::Groups::Pipelines::GroupPipeline, context: context
- expect_to_run_pipeline('EE::BulkImports::Groups::Pipelines::EpicsPipeline'.constantize, context: context) if Gitlab.ee?
expect_to_run_pipeline BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline, context: context
+ expect_to_run_pipeline BulkImports::Groups::Pipelines::MembersPipeline, context: context
+ expect_to_run_pipeline BulkImports::Groups::Pipelines::LabelsPipeline, context: context
+
+ if Gitlab.ee?
+ expect_to_run_pipeline('EE::BulkImports::Groups::Pipelines::EpicsPipeline'.constantize, context: context)
+ expect_to_run_pipeline('EE::BulkImports::Groups::Pipelines::EpicAwardEmojiPipeline'.constantize, context: context)
+ end
subject.execute
@@ -33,7 +34,7 @@ RSpec.describe BulkImports::Importers::GroupImporter do
end
context 'when failed' do
- let(:bulk_import_entity) { create(:bulk_import_entity, :failed, bulk_import: bulk_import) }
+ let(:bulk_import_entity) { create(:bulk_import_entity, :failed, bulk_import: bulk_import, group: group) }
it 'does not transition entity to finished state' do
allow(bulk_import_entity).to receive(:start!)
@@ -46,8 +47,8 @@ RSpec.describe BulkImports::Importers::GroupImporter do
end
def expect_to_run_pipeline(klass, context:)
- expect_next_instance_of(klass) do |pipeline|
- expect(pipeline).to receive(:run).with(context)
+ expect_next_instance_of(klass, context) do |pipeline|
+ expect(pipeline).to receive(:run)
end
end
end
diff --git a/spec/lib/bulk_imports/pipeline/context_spec.rb b/spec/lib/bulk_imports/pipeline/context_spec.rb
index e9af6313ca4..c8c3fe3a861 100644
--- a/spec/lib/bulk_imports/pipeline/context_spec.rb
+++ b/spec/lib/bulk_imports/pipeline/context_spec.rb
@@ -3,25 +3,29 @@
require 'spec_helper'
RSpec.describe BulkImports::Pipeline::Context do
- describe '#initialize' do
- it 'initializes with permitted attributes' do
- args = {
- current_user: create(:user),
- entity: create(:bulk_import_entity),
- configuration: create(:bulk_import_configuration)
- }
+ let(:group) { instance_double(Group) }
+ let(:user) { instance_double(User) }
+ let(:bulk_import) { instance_double(BulkImport, user: user, configuration: :config) }
- context = described_class.new(args)
+ let(:entity) do
+ instance_double(
+ BulkImports::Entity,
+ bulk_import: bulk_import,
+ group: group
+ )
+ end
+
+ subject { described_class.new(entity) }
- args.each do |k, v|
- expect(context.public_send(k)).to eq(v)
- end
- end
+ describe '#group' do
+ it { expect(subject.group).to eq(group) }
+ end
+
+ describe '#current_user' do
+ it { expect(subject.current_user).to eq(user) }
+ end
- context 'when invalid argument is passed' do
- it 'raises NoMethodError' do
- expect { described_class.new(test: 'test').test }.to raise_exception(NoMethodError)
- end
- end
+ describe '#current_user' do
+ it { expect(subject.configuration).to eq(bulk_import.configuration) }
end
end
diff --git a/spec/lib/bulk_imports/pipeline/extracted_data_spec.rb b/spec/lib/bulk_imports/pipeline/extracted_data_spec.rb
new file mode 100644
index 00000000000..25c5178227a
--- /dev/null
+++ b/spec/lib/bulk_imports/pipeline/extracted_data_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Pipeline::ExtractedData do
+ let(:data) { 'data' }
+ let(:has_next_page) { true }
+ let(:cursor) { 'cursor' }
+ let(:page_info) do
+ {
+ 'has_next_page' => has_next_page,
+ 'end_cursor' => cursor
+ }
+ end
+
+ subject { described_class.new(data: data, page_info: page_info) }
+
+ describe '#has_next_page?' do
+ context 'when next page is present' do
+ it 'returns true' do
+ expect(subject.has_next_page?).to eq(true)
+ end
+ end
+
+ context 'when next page is not present' do
+ let(:has_next_page) { false }
+
+ it 'returns false' do
+ expect(subject.has_next_page?).to eq(false)
+ end
+ end
+ end
+
+ describe '#next_page' do
+ it 'returns next page cursor information' do
+ expect(subject.next_page).to eq(cursor)
+ end
+ end
+
+ describe '#each' do
+ context 'when block is present' do
+ it 'yields each data item' do
+ expect { |b| subject.each(&b) }.to yield_control
+ end
+ end
+
+ context 'when block is not present' do
+ it 'returns enumerator' do
+ expect(subject.each).to be_instance_of(Enumerator)
+ end
+ end
+ end
+end
diff --git a/spec/lib/bulk_imports/pipeline/runner_spec.rb b/spec/lib/bulk_imports/pipeline/runner_spec.rb
index 60833e83dcc..76e4e64a7d6 100644
--- a/spec/lib/bulk_imports/pipeline/runner_spec.rb
+++ b/spec/lib/bulk_imports/pipeline/runner_spec.rb
@@ -39,56 +39,94 @@ RSpec.describe BulkImports::Pipeline::Runner do
extractor BulkImports::Extractor
transformer BulkImports::Transformer
loader BulkImports::Loader
+
+ def after_run(_); end
end
stub_const('BulkImports::MyPipeline', pipeline)
end
context 'when entity is not marked as failed' do
- let(:context) do
- instance_double(
- BulkImports::Pipeline::Context,
- entity: instance_double(BulkImports::Entity, id: 1, source_type: 'group', failed?: false)
- )
- end
+ let(:entity) { create(:bulk_import_entity) }
+ let(:context) { BulkImports::Pipeline::Context.new(entity) }
it 'runs pipeline extractor, transformer, loader' do
- entries = [{ foo: :bar }]
+ extracted_data = BulkImports::Pipeline::ExtractedData.new(data: { foo: :bar })
expect_next_instance_of(BulkImports::Extractor) do |extractor|
- expect(extractor).to receive(:extract).with(context).and_return(entries)
+ expect(extractor)
+ .to receive(:extract)
+ .with(context)
+ .and_return(extracted_data)
end
expect_next_instance_of(BulkImports::Transformer) do |transformer|
- expect(transformer).to receive(:transform).with(context, entries.first).and_return(entries.first)
+ expect(transformer)
+ .to receive(:transform)
+ .with(context, extracted_data.data.first)
+ .and_return(extracted_data.data.first)
end
expect_next_instance_of(BulkImports::Loader) do |loader|
- expect(loader).to receive(:load).with(context, entries.first)
+ expect(loader)
+ .to receive(:load)
+ .with(context, extracted_data.data.first)
end
expect_next_instance_of(Gitlab::Import::Logger) do |logger|
expect(logger).to receive(:info)
.with(
+ bulk_import_entity_id: entity.id,
+ bulk_import_entity_type: 'group_entity',
message: 'Pipeline started',
+ pipeline_class: 'BulkImports::MyPipeline'
+ )
+ expect(logger).to receive(:info)
+ .with(
+ bulk_import_entity_id: entity.id,
+ bulk_import_entity_type: 'group_entity',
pipeline_class: 'BulkImports::MyPipeline',
- bulk_import_entity_id: 1,
- bulk_import_entity_type: 'group'
+ pipeline_step: :extractor,
+ step_class: 'BulkImports::Extractor'
)
expect(logger).to receive(:info)
- .with(bulk_import_entity_id: 1, bulk_import_entity_type: 'group', extractor: 'BulkImports::Extractor')
+ .with(
+ bulk_import_entity_id: entity.id,
+ bulk_import_entity_type: 'group_entity',
+ pipeline_class: 'BulkImports::MyPipeline',
+ pipeline_step: :transformer,
+ step_class: 'BulkImports::Transformer'
+ )
expect(logger).to receive(:info)
- .with(bulk_import_entity_id: 1, bulk_import_entity_type: 'group', transformer: 'BulkImports::Transformer')
+ .with(
+ bulk_import_entity_id: entity.id,
+ bulk_import_entity_type: 'group_entity',
+ pipeline_class: 'BulkImports::MyPipeline',
+ pipeline_step: :loader,
+ step_class: 'BulkImports::Loader'
+ )
+ expect(logger).to receive(:info)
+ .with(
+ bulk_import_entity_id: entity.id,
+ bulk_import_entity_type: 'group_entity',
+ pipeline_class: 'BulkImports::MyPipeline',
+ pipeline_step: :after_run
+ )
expect(logger).to receive(:info)
- .with(bulk_import_entity_id: 1, bulk_import_entity_type: 'group', loader: 'BulkImports::Loader')
+ .with(
+ bulk_import_entity_id: entity.id,
+ bulk_import_entity_type: 'group_entity',
+ message: 'Pipeline finished',
+ pipeline_class: 'BulkImports::MyPipeline'
+ )
end
- BulkImports::MyPipeline.new.run(context)
+ BulkImports::MyPipeline.new(context).run
end
context 'when exception is raised' do
let(:entity) { create(:bulk_import_entity, :created) }
- let(:context) { BulkImports::Pipeline::Context.new(entity: entity) }
+ let(:context) { BulkImports::Pipeline::Context.new(entity) }
before do
allow_next_instance_of(BulkImports::Extractor) do |extractor|
@@ -97,12 +135,13 @@ RSpec.describe BulkImports::Pipeline::Runner do
end
it 'logs import failure' do
- BulkImports::MyPipeline.new.run(context)
+ BulkImports::MyPipeline.new(context).run
failure = entity.failures.first
expect(failure).to be_present
expect(failure.pipeline_class).to eq('BulkImports::MyPipeline')
+ expect(failure.pipeline_step).to eq('extractor')
expect(failure.exception_class).to eq('StandardError')
expect(failure.exception_message).to eq('Error!')
end
@@ -113,7 +152,7 @@ RSpec.describe BulkImports::Pipeline::Runner do
end
it 'marks entity as failed' do
- BulkImports::MyPipeline.new.run(context)
+ BulkImports::MyPipeline.new(context).run
expect(entity.failed?).to eq(true)
end
@@ -129,13 +168,13 @@ RSpec.describe BulkImports::Pipeline::Runner do
)
end
- BulkImports::MyPipeline.new.run(context)
+ BulkImports::MyPipeline.new(context).run
end
end
context 'when pipeline is not marked to abort on failure' do
it 'marks entity as failed' do
- BulkImports::MyPipeline.new.run(context)
+ BulkImports::MyPipeline.new(context).run
expect(entity.failed?).to eq(false)
end
@@ -144,25 +183,23 @@ RSpec.describe BulkImports::Pipeline::Runner do
end
context 'when entity is marked as failed' do
- let(:context) do
- instance_double(
- BulkImports::Pipeline::Context,
- entity: instance_double(BulkImports::Entity, id: 1, source_type: 'group', failed?: true)
- )
- end
+ let(:entity) { create(:bulk_import_entity) }
+ let(:context) { BulkImports::Pipeline::Context.new(entity) }
it 'logs and returns without execution' do
+ allow(entity).to receive(:failed?).and_return(true)
+
expect_next_instance_of(Gitlab::Import::Logger) do |logger|
expect(logger).to receive(:info)
.with(
message: 'Skipping due to failed pipeline status',
pipeline_class: 'BulkImports::MyPipeline',
- bulk_import_entity_id: 1,
- bulk_import_entity_type: 'group'
+ bulk_import_entity_id: entity.id,
+ bulk_import_entity_type: 'group_entity'
)
end
- BulkImports::MyPipeline.new.run(context)
+ BulkImports::MyPipeline.new(context).run
end
end
end
diff --git a/spec/lib/feature/gitaly_spec.rb b/spec/lib/feature/gitaly_spec.rb
index a2181a63335..696427bb8b6 100644
--- a/spec/lib/feature/gitaly_spec.rb
+++ b/spec/lib/feature/gitaly_spec.rb
@@ -3,35 +3,78 @@
require 'spec_helper'
RSpec.describe Feature::Gitaly do
- let(:feature_flag) { "mep_mep" }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:project_2) { create(:project) }
+
+ before do
+ skip_feature_flags_yaml_validation
+ end
describe ".enabled?" do
- context 'when the gate is closed' do
- before do
- stub_feature_flags(gitaly_mep_mep: false)
+ 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)
+ end
end
- it 'returns false' do
- expect(described_class.enabled?(feature_flag)).to be(false)
+ context 'when the flag defaults to on' do
+ it 'returns true' do
+ expect(described_class.enabled?(feature_flag)).to be(true)
+ end
end
end
- context 'when the flag defaults to on' do
- it 'returns true' do
- expect(described_class.enabled?(feature_flag)).to be(true)
+ 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)
+ end
+
+ it 'returns false for any other project' do
+ expect(described_class.enabled?(feature_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)
end
end
end
describe ".server_feature_flags" do
before do
- stub_feature_flags(gitaly_mep_mep: true, foo: true)
+ stub_feature_flags(gitaly_global_flag: true, gitaly_project_flag: project, non_gitaly_flag: false)
end
subject { described_class.server_feature_flags }
- it { is_expected.to be_a(Hash) }
- it { is_expected.to eq("gitaly-feature-mep-mep" => "true") }
+ 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')
+ 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))
+ .to eq('gitaly-feature-global-flag' => 'true',
+ 'gitaly-feature-project-flag' => 'true')
+
+ expect(described_class.server_feature_flags(project_2))
+ .to eq('gitaly-feature-global-flag' => 'true',
+ 'gitaly-feature-project-flag' => 'false')
+ end
+ end
context 'when table does not exist' do
before do
diff --git a/spec/lib/gitlab/access/branch_protection_spec.rb b/spec/lib/gitlab/access/branch_protection_spec.rb
index 9b736a30c7e..44c30d1f596 100644
--- a/spec/lib/gitlab/access/branch_protection_spec.rb
+++ b/spec/lib/gitlab/access/branch_protection_spec.rb
@@ -3,9 +3,9 @@
require 'spec_helper'
RSpec.describe Gitlab::Access::BranchProtection do
- describe '#any?' do
- using RSpec::Parameterized::TableSyntax
+ using RSpec::Parameterized::TableSyntax
+ describe '#any?' do
where(:level, :result) do
Gitlab::Access::PROTECTION_NONE | false
Gitlab::Access::PROTECTION_DEV_CAN_PUSH | true
@@ -19,8 +19,6 @@ RSpec.describe Gitlab::Access::BranchProtection do
end
describe '#developer_can_push?' do
- using RSpec::Parameterized::TableSyntax
-
where(:level, :result) do
Gitlab::Access::PROTECTION_NONE | false
Gitlab::Access::PROTECTION_DEV_CAN_PUSH | true
@@ -36,8 +34,6 @@ RSpec.describe Gitlab::Access::BranchProtection do
end
describe '#developer_can_merge?' do
- using RSpec::Parameterized::TableSyntax
-
where(:level, :result) do
Gitlab::Access::PROTECTION_NONE | false
Gitlab::Access::PROTECTION_DEV_CAN_PUSH | false
@@ -53,8 +49,6 @@ RSpec.describe Gitlab::Access::BranchProtection do
end
describe '#fully_protected?' do
- using RSpec::Parameterized::TableSyntax
-
where(:level, :result) do
Gitlab::Access::PROTECTION_NONE | false
Gitlab::Access::PROTECTION_DEV_CAN_PUSH | false
diff --git a/spec/lib/gitlab/alert_management/payload/generic_spec.rb b/spec/lib/gitlab/alert_management/payload/generic_spec.rb
index b7660462b0d..d022c629458 100644
--- a/spec/lib/gitlab/alert_management/payload/generic_spec.rb
+++ b/spec/lib/gitlab/alert_management/payload/generic_spec.rb
@@ -19,7 +19,34 @@ RSpec.describe Gitlab::AlertManagement::Payload::Generic do
describe '#severity' do
subject { parsed_payload.severity }
- it_behaves_like 'parsable alert payload field with fallback', 'critical', 'severity'
+ context 'when set' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:raw_payload) { { 'severity' => payload_severity } }
+
+ where(:payload_severity, :expected_severity) do
+ 'critical' | :critical
+ 'high' | :high
+ 'medium' | :medium
+ 'low' | :low
+ 'info' | :info
+
+ 'CRITICAL' | :critical
+ 'cRiTiCaL' | :critical
+
+ 'unmapped' | nil
+ 1 | nil
+ nil | nil
+ end
+
+ with_them do
+ it { is_expected.to eq(expected_severity) }
+ end
+ end
+
+ context 'without key' do
+ it { is_expected.to be_nil }
+ end
end
describe '#monitoring_tool' do
diff --git a/spec/lib/gitlab/alert_management/payload/prometheus_spec.rb b/spec/lib/gitlab/alert_management/payload/prometheus_spec.rb
index 457db58a28b..f574f5ba6a3 100644
--- a/spec/lib/gitlab/alert_management/payload/prometheus_spec.rb
+++ b/spec/lib/gitlab/alert_management/payload/prometheus_spec.rb
@@ -156,8 +156,6 @@ RSpec.describe Gitlab::AlertManagement::Payload::Prometheus do
end
describe '#gitlab_fingerprint' do
- subject { parsed_payload.gitlab_fingerprint }
-
let(:raw_payload) do
{
'startsAt' => Time.current.to_s,
@@ -166,6 +164,8 @@ RSpec.describe Gitlab::AlertManagement::Payload::Prometheus do
}
end
+ subject { parsed_payload.gitlab_fingerprint }
+
it 'returns a fingerprint' do
plain_fingerprint = [
parsed_payload.send(:starts_at_raw),
@@ -237,4 +237,63 @@ RSpec.describe Gitlab::AlertManagement::Payload::Prometheus do
it { is_expected.to be_falsey }
end
end
+
+ describe '#severity' do
+ subject { parsed_payload.severity }
+
+ context 'when set' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:raw_payload) { { 'labels' => { 'severity' => payload_severity } } }
+
+ where(:payload_severity, :expected_severity) do
+ 'critical' | :critical
+ 'high' | :high
+ 'medium' | :medium
+ 'low' | :low
+ 'info' | :info
+
+ 's1' | :critical
+ 's2' | :high
+ 's3' | :medium
+ 's4' | :low
+ 's5' | :info
+ 'p1' | :critical
+ 'p2' | :high
+ 'p3' | :medium
+ 'p4' | :low
+ 'p5' | :info
+
+ 'CRITICAL' | :critical
+ 'cRiTiCaL' | :critical
+ 'S1' | :critical
+
+ 'unmapped' | nil
+ 1 | nil
+ nil | nil
+
+ 'debug' | :info
+ 'information' | :info
+ 'notice' | :info
+ 'warn' | :low
+ 'warning' | :low
+ 'minor' | :low
+ 'error' | :medium
+ 'major' | :high
+ 'emergency' | :critical
+ 'fatal' | :critical
+
+ 'alert' | :medium
+ 'page' | :high
+ end
+
+ with_them do
+ it { is_expected.to eq(expected_severity) }
+ end
+ end
+
+ context 'without key' do
+ it { is_expected.to be_nil }
+ end
+ end
end
diff --git a/spec/lib/gitlab/alert_management/payload_spec.rb b/spec/lib/gitlab/alert_management/payload_spec.rb
index 44b55e228c5..7c129a8a48e 100644
--- a/spec/lib/gitlab/alert_management/payload_spec.rb
+++ b/spec/lib/gitlab/alert_management/payload_spec.rb
@@ -56,5 +56,20 @@ RSpec.describe Gitlab::AlertManagement::Payload do
it { is_expected.to be_a Gitlab::AlertManagement::Payload::Generic }
end
end
+
+ context 'with integration specified by caller' do
+ let(:integration) { instance_double(AlertManagement::HttpIntegration) }
+
+ subject { described_class.parse(project, payload, integration: integration) }
+
+ it 'passes an integration to a specific payload' do
+ expect(::Gitlab::AlertManagement::Payload::Generic)
+ .to receive(:new)
+ .with(project: project, payload: payload, integration: integration)
+ .and_call_original
+
+ subject
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/api_authentication/token_locator_spec.rb b/spec/lib/gitlab/api_authentication/token_locator_spec.rb
index 68ce48a70ea..e933fd8352e 100644
--- a/spec/lib/gitlab/api_authentication/token_locator_spec.rb
+++ b/spec/lib/gitlab/api_authentication/token_locator_spec.rb
@@ -51,5 +51,26 @@ RSpec.describe Gitlab::APIAuthentication::TokenLocator do
end
end
end
+
+ context 'with :http_token' do
+ let(:type) { :http_token }
+
+ context 'without credentials' do
+ let(:request) { double(headers: {}) }
+
+ it 'returns nil' do
+ expect(subject).to be(nil)
+ end
+ end
+
+ context 'with credentials' do
+ let(:password) { 'bar' }
+ let(:request) { double(headers: { "Authorization" => password }) }
+
+ it 'returns the credentials' do
+ expect(subject.password).to eq(password)
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/api_authentication/token_resolver_spec.rb b/spec/lib/gitlab/api_authentication/token_resolver_spec.rb
index 0028fb080ac..97a7c8ba7cf 100644
--- a/spec/lib/gitlab/api_authentication/token_resolver_spec.rb
+++ b/spec/lib/gitlab/api_authentication/token_resolver_spec.rb
@@ -47,8 +47,8 @@ RSpec.describe Gitlab::APIAuthentication::TokenResolver do
subject { resolver.resolve(raw) }
- context 'with :personal_access_token' do
- let(:type) { :personal_access_token }
+ context 'with :personal_access_token_with_username' do
+ let(:type) { :personal_access_token_with_username }
let(:token) { personal_access_token }
context 'with valid credentials' do
@@ -62,10 +62,16 @@ RSpec.describe Gitlab::APIAuthentication::TokenResolver do
it_behaves_like 'an unauthorized request'
end
+
+ context 'with no username' do
+ let(:raw) { username_and_password(nil, token.token) }
+
+ it_behaves_like 'an unauthorized request'
+ end
end
- context 'with :job_token' do
- let(:type) { :job_token }
+ context 'with :job_token_with_username' do
+ let(:type) { :job_token_with_username }
let(:token) { ci_job }
context 'with valid credentials' do
@@ -93,8 +99,8 @@ RSpec.describe Gitlab::APIAuthentication::TokenResolver do
end
end
- context 'with :deploy_token' do
- let(:type) { :deploy_token }
+ context 'with :deploy_token_with_username' do
+ let(:type) { :deploy_token_with_username }
let(:token) { deploy_token }
context 'with a valid deploy token' do
@@ -109,6 +115,51 @@ RSpec.describe Gitlab::APIAuthentication::TokenResolver do
it_behaves_like 'an unauthorized request'
end
end
+
+ context 'with :personal_access_token' do
+ let(:type) { :personal_access_token }
+ let(:token) { personal_access_token }
+
+ context 'with valid credentials' do
+ let(:raw) { username_and_password(nil, token.token) }
+
+ it_behaves_like 'an authorized request'
+ end
+ end
+
+ context 'with :job_token' do
+ let(:type) { :job_token }
+ let(:token) { ci_job }
+
+ context 'with valid credentials' do
+ let(:raw) { username_and_password(nil, token.token) }
+
+ it_behaves_like 'an authorized request'
+ end
+
+ context 'when the job is not running' do
+ let(:raw) { username_and_password(nil, ci_job_done.token) }
+
+ it_behaves_like 'an unauthorized request'
+ end
+
+ context 'with an invalid job token' do
+ let(:raw) { username_and_password(nil, "not a valid CI job token") }
+
+ it_behaves_like 'an unauthorized request'
+ end
+ end
+
+ context 'with :deploy_token' do
+ let(:type) { :deploy_token }
+ let(:token) { deploy_token }
+
+ context 'with a valid deploy token' do
+ let(:raw) { username_and_password(nil, token.token) }
+
+ it_behaves_like 'an authorized request'
+ end
+ end
end
def username_and_password(username, password)
diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index 36e4decdead..08510d4652b 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -510,6 +510,73 @@ module Gitlab
expect(render(input, context)).to include(output.strip)
end
+
+ it 'does not convert a blockdiag diagram to image' do
+ input = <<~ADOC
+ [blockdiag]
+ ....
+ blockdiag {
+ Kroki -> generates -> "Block diagrams";
+ Kroki -> is -> "very easy!";
+
+ Kroki [color = "greenyellow"];
+ "Block diagrams" [color = "pink"];
+ "very easy!" [color = "orange"];
+ }
+ ....
+ ADOC
+
+ output = <<~HTML
+ <div>
+ <div>
+ <pre>blockdiag {
+ Kroki -&gt; generates -&gt; "Block diagrams";
+ Kroki -&gt; is -&gt; "very easy!";
+
+ Kroki [color = "greenyellow"];
+ "Block diagrams" [color = "pink"];
+ "very easy!" [color = "orange"];
+ }</pre>
+ </div>
+ </div>
+ HTML
+
+ expect(render(input, context)).to include(output.strip)
+ end
+ end
+
+ context 'with Kroki and BlockDiag (additional format) enabled' do
+ before do
+ allow_any_instance_of(ApplicationSetting).to receive(:kroki_enabled).and_return(true)
+ allow_any_instance_of(ApplicationSetting).to receive(:kroki_url).and_return('https://kroki.io')
+ allow_any_instance_of(ApplicationSetting).to receive(:kroki_formats_blockdiag).and_return(true)
+ end
+
+ it 'converts a blockdiag diagram to image' do
+ input = <<~ADOC
+ [blockdiag]
+ ....
+ blockdiag {
+ Kroki -> generates -> "Block diagrams";
+ Kroki -> is -> "very easy!";
+
+ Kroki [color = "greenyellow"];
+ "Block diagrams" [color = "pink"];
+ "very easy!" [color = "orange"];
+ }
+ ....
+ ADOC
+
+ output = <<~HTML
+ <div>
+ <div>
+ <a class="no-attachment-icon" href="https://kroki.io/blockdiag/svg/eNpdzDEKQjEQhOHeU4zpPYFoYesRxGJ9bwghMSsbUYJ4d10UCZbDfPynolOek0Q8FsDeNCestoisNLmy-Qg7R3Blcm5hPcr0ITdaB6X15fv-_YdJixo2CNHI2lmK3sPRA__RwV5SzV80ZAegJjXSyfMFptc71w==" target="_blank" rel="noopener noreferrer"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Diagram" class="lazy" data-src="https://kroki.io/blockdiag/svg/eNpdzDEKQjEQhOHeU4zpPYFoYesRxGJ9bwghMSsbUYJ4d10UCZbDfPynolOek0Q8FsDeNCestoisNLmy-Qg7R3Blcm5hPcr0ITdaB6X15fv-_YdJixo2CNHI2lmK3sPRA__RwV5SzV80ZAegJjXSyfMFptc71w=="></a>
+ </div>
+ </div>
+ HTML
+
+ expect(render(input, context)).to include(output.strip)
+ end
end
end
diff --git a/spec/lib/gitlab/auth/auth_finders_spec.rb b/spec/lib/gitlab/auth/auth_finders_spec.rb
index 775f8f056b5..cddcaf09b74 100644
--- a/spec/lib/gitlab/auth/auth_finders_spec.rb
+++ b/spec/lib/gitlab/auth/auth_finders_spec.rb
@@ -7,7 +7,18 @@ RSpec.describe Gitlab::Auth::AuthFinders do
include HttpBasicAuthHelpers
# Create the feed_token and static_object_token for the user
- let_it_be(:user) { create(:user).tap(&:feed_token).tap(&:static_object_token) }
+ let_it_be(:user, freeze: true) { create(:user).tap(&:feed_token).tap(&:static_object_token) }
+ let_it_be(:personal_access_token, freeze: true) { create(:personal_access_token, user: user) }
+
+ let_it_be(:project, freeze: true) { create(:project, :private) }
+ let_it_be(:pipeline, freeze: true) { create(:ci_pipeline, project: project) }
+ let_it_be(:job, freeze: true) { create(:ci_build, :running, pipeline: pipeline, user: user) }
+ let_it_be(:failed_job, freeze: true) { create(:ci_build, :failed, pipeline: pipeline, user: user) }
+
+ let_it_be(:project2, freeze: true) { create(:project, :private) }
+ let_it_be(:pipeline2, freeze: true) { create(:ci_pipeline, project: project2) }
+ let_it_be(:job2, freeze: true) { create(:ci_build, :running, pipeline: pipeline2, user: user) }
+
let(:env) do
{
'rack.input' => ''
@@ -15,6 +26,12 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
let(:request) { ActionDispatch::Request.new(env) }
+ let(:params) { {} }
+
+ before_all do
+ project.add_developer(user)
+ project2.add_developer(user)
+ end
def set_param(key, value)
request.update_param(key, value)
@@ -28,75 +45,93 @@ RSpec.describe Gitlab::Auth::AuthFinders do
env.merge!(basic_auth_header(username, password))
end
- shared_examples 'find user from job token' do
+ def set_bearer_token(token)
+ env['HTTP_AUTHORIZATION'] = "Bearer #{token}"
+ end
+
+ shared_examples 'find user from job token' do |without_job_token_allowed|
context 'when route is allowed to be authenticated' do
let(:route_authentication_setting) { { job_token_allowed: true } }
- it "returns an Unauthorized exception for an invalid token" do
- set_token('invalid token')
+ context 'for an invalid token' do
+ let(:token) { 'invalid token' }
- expect { subject }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ it "returns an Unauthorized exception" do
+ expect { subject }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ expect(@current_authenticated_job).to be_nil
+ end
end
context 'with a running job' do
- before do
- job.update!(status: :running)
- end
-
- it 'return user if token is valid' do
- set_token(job.token)
+ let(:token) { job.token }
+ it 'return user' do
expect(subject).to eq(user)
expect(@current_authenticated_job).to eq job
end
end
context 'with a job that is not running' do
- before do
- job.update!(status: :failed)
- end
+ let(:token) { failed_job.token }
it 'returns an Unauthorized exception' do
- set_token(job.token)
-
expect { subject }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ expect(@current_authenticated_job).to be_nil
+ end
+ end
+ end
+
+ context 'when route is not allowed to be authenticated' do
+ let(:route_authentication_setting) { { job_token_allowed: false } }
+
+ context 'with a running job' do
+ let(:token) { job.token }
+
+ if without_job_token_allowed == :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
+ it 'returns the user' do
+ expect(subject).to eq(user)
+ expect(@current_authenticated_job).to eq job
+ end
+ else
+ it 'returns nil' do
+ is_expected.to be_nil
+ expect(@current_authenticated_job).to be_nil
+ end
end
end
end
end
describe '#find_user_from_bearer_token' do
- let_it_be_with_reload(:job) { create(:ci_build, user: user) }
-
subject { find_user_from_bearer_token }
context 'when the token is passed as an oauth token' do
- def set_token(token)
- env['HTTP_AUTHORIZATION'] = "Bearer #{token}"
+ before do
+ set_bearer_token(token)
end
- context 'with a job token' do
- it_behaves_like 'find user from job token'
- end
+ it_behaves_like 'find user from job token', :error
+ end
- context 'with oauth token' do
- let(:application) { Doorkeeper::Application.create!(name: 'MyApp', redirect_uri: 'https://app.com', owner: user) }
- let(:token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: 'api').token }
+ context 'with oauth token' do
+ let(:application) { Doorkeeper::Application.create!(name: 'MyApp', redirect_uri: 'https://app.com', owner: user) }
+ let(:doorkeeper_access_token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: 'api') }
- before do
- set_token(token)
- end
-
- it { is_expected.to eq user }
+ before do
+ set_bearer_token(doorkeeper_access_token.token)
end
+
+ it { is_expected.to eq user }
end
context 'with a personal access token' do
- let_it_be(:pat) { create(:personal_access_token, user: user) }
- let(:token) { pat.token }
-
before do
- env[described_class::PRIVATE_TOKEN_HEADER] = pat.token
+ env[described_class::PRIVATE_TOKEN_HEADER] = personal_access_token.token
end
it { is_expected.to eq user }
@@ -277,7 +312,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
describe '#deploy_token_from_request' do
- let_it_be(:deploy_token) { create(:deploy_token) }
+ let_it_be(:deploy_token, freeze: true) { create(:deploy_token) }
let_it_be(:route_authentication_setting) { { deploy_token_allowed: true } }
subject { deploy_token_from_request }
@@ -293,11 +328,13 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
context 'with deploy token headers' do
- before do
- set_header(described_class::DEPLOY_TOKEN_HEADER, deploy_token.token)
- end
+ context 'with valid deploy token' do
+ before do
+ set_header(described_class::DEPLOY_TOKEN_HEADER, deploy_token.token)
+ end
- it { is_expected.to eq deploy_token }
+ it { is_expected.to eq deploy_token }
+ end
it_behaves_like 'an unauthenticated route'
@@ -311,17 +348,19 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
context 'with oauth headers' do
- before do
- set_header('HTTP_AUTHORIZATION', "Bearer #{deploy_token.token}")
- end
+ context 'with valid token' do
+ before do
+ set_bearer_token(deploy_token.token)
+ end
- it { is_expected.to eq deploy_token }
+ it { is_expected.to eq deploy_token }
- it_behaves_like 'an unauthenticated route'
+ it_behaves_like 'an unauthenticated route'
+ end
context 'with invalid token' do
before do
- set_header('HTTP_AUTHORIZATION', "Bearer invalid_token")
+ set_bearer_token('invalid_token')
end
it { is_expected.to be_nil }
@@ -348,8 +387,6 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
describe '#find_user_from_access_token' do
- let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
-
before do
set_header('SCRIPT_NAME', 'url.atom')
end
@@ -374,24 +411,34 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
context 'with OAuth headers' do
- it 'returns user' do
- set_header('HTTP_AUTHORIZATION', "Bearer #{personal_access_token.token}")
+ context 'with valid personal access token' do
+ before do
+ set_bearer_token(personal_access_token.token)
+ end
- expect(find_user_from_access_token).to eq user
+ it 'returns user' do
+ expect(find_user_from_access_token).to eq user
+ end
end
- it 'returns exception if invalid personal_access_token' do
- env['HTTP_AUTHORIZATION'] = 'Bearer invalid_20byte_token'
+ context 'with invalid personal_access_token' do
+ before do
+ set_bearer_token('invalid_20byte_token')
+ end
- expect { find_personal_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ it 'returns exception' do
+ expect { find_personal_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ end
end
context 'when using a non-prefixed access token' do
- let_it_be(:personal_access_token) { create(:personal_access_token, :no_prefix, user: user) }
+ let_it_be(:personal_access_token, freeze: true) { create(:personal_access_token, :no_prefix, user: user) }
- it 'returns user' do
- set_header('HTTP_AUTHORIZATION', "Bearer #{personal_access_token.token}")
+ before do
+ set_bearer_token(personal_access_token.token)
+ end
+ it 'returns user' do
expect(find_user_from_access_token).to eq user
end
end
@@ -399,8 +446,6 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
describe '#find_user_from_web_access_token' do
- let_it_be_with_reload(:personal_access_token) { create(:personal_access_token, user: user) }
-
before do
set_header(described_class::PRIVATE_TOKEN_HEADER, personal_access_token.token)
end
@@ -451,9 +496,9 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
context 'when the token has read_api scope' do
- before do
- personal_access_token.update!(scopes: ['read_api'])
+ let_it_be(:personal_access_token, freeze: true) { create(:personal_access_token, user: user, scopes: ['read_api']) }
+ before do
set_header('SCRIPT_NAME', '/api/endpoint')
end
@@ -481,8 +526,6 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
describe '#find_personal_access_token' do
- let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
-
before do
set_header('SCRIPT_NAME', 'url.atom')
end
@@ -516,21 +559,23 @@ RSpec.describe Gitlab::Auth::AuthFinders do
describe '#find_oauth_access_token' do
let(:application) { Doorkeeper::Application.create!(name: 'MyApp', redirect_uri: 'https://app.com', owner: user) }
- let(:token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: 'api') }
+ let(:doorkeeper_access_token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: 'api') }
context 'passed as header' do
- it 'returns token if valid oauth_access_token' do
- set_header('HTTP_AUTHORIZATION', "Bearer #{token.token}")
+ before do
+ set_bearer_token(doorkeeper_access_token.token)
+ end
- expect(find_oauth_access_token.token).to eq token.token
+ it 'returns token if valid oauth_access_token' do
+ expect(find_oauth_access_token.token).to eq doorkeeper_access_token.token
end
end
context 'passed as param' do
it 'returns user if valid oauth_access_token' do
- set_param(:access_token, token.token)
+ set_param(:access_token, doorkeeper_access_token.token)
- expect(find_oauth_access_token.token).to eq token.token
+ expect(find_oauth_access_token.token).to eq doorkeeper_access_token.token
end
end
@@ -538,10 +583,14 @@ RSpec.describe Gitlab::Auth::AuthFinders do
expect(find_oauth_access_token).to be_nil
end
- it 'returns exception if invalid oauth_access_token' do
- set_header('HTTP_AUTHORIZATION', "Bearer invalid_token")
+ context 'with invalid token' do
+ before do
+ set_bearer_token('invalid_token')
+ end
- expect { find_oauth_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ it 'returns exception if invalid oauth_access_token' do
+ expect { find_oauth_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ end
end
end
@@ -551,7 +600,6 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
context 'access token is valid' do
- let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
let(:route_authentication_setting) { { basic_auth_personal_access_token: true } }
it 'finds the token from basic auth' do
@@ -572,8 +620,6 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
context 'route_setting is not set' do
- let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
-
it 'returns nil' do
auth_header_with(personal_access_token.token)
@@ -582,7 +628,6 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
context 'route_setting is not correct' do
- let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
let(:route_authentication_setting) { { basic_auth_personal_access_token: false } }
it 'returns nil' do
@@ -629,44 +674,18 @@ RSpec.describe Gitlab::Auth::AuthFinders do
context 'with CI username' do
let(:username) { ::Gitlab::Auth::CI_JOB_USER }
- let_it_be(:user) { create(:user) }
- let_it_be(:build) { create(:ci_build, user: user, status: :running) }
-
- it 'returns nil without password' do
- set_basic_auth_header(username, nil)
-
- is_expected.to be_nil
- end
-
- it 'returns user with valid token' do
- set_basic_auth_header(username, build.token)
-
- is_expected.to eq user
- expect(@current_authenticated_job).to eq build
- end
-
- it 'raises error with invalid token' do
- set_basic_auth_header(username, 'token')
-
- expect { subject }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ before do
+ set_basic_auth_header(username, token)
end
- it 'returns exception if the job is not running' do
- set_basic_auth_header(username, build.token)
- build.success!
-
- expect { subject }.to raise_error(Gitlab::Auth::UnauthorizedError)
- end
+ it_behaves_like 'find user from job token', :user
end
end
describe '#validate_access_token!' do
subject { validate_access_token! }
- let_it_be_with_reload(:personal_access_token) { create(:personal_access_token, user: user) }
-
context 'with a job token' do
- let_it_be(:job) { create(:ci_build, user: user, status: :running) }
let(:route_authentication_setting) { { job_token_allowed: true } }
before do
@@ -684,6 +703,8 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
context 'token is not valid' do
+ let_it_be_with_reload(:personal_access_token) { create(:personal_access_token, user: user) }
+
before do
allow_any_instance_of(described_class).to receive(:access_token).and_return(personal_access_token)
end
@@ -706,7 +727,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
context 'with impersonation token' do
- let_it_be(:personal_access_token) { create(:personal_access_token, :impersonation, user: user) }
+ let_it_be(:personal_access_token, freeze: true) { create(:personal_access_token, :impersonation, user: user) }
context 'when impersonation is disabled' do
before do
@@ -722,96 +743,30 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
describe '#find_user_from_job_token' do
- let_it_be(:job) { create(:ci_build, user: user, status: :running) }
- let(:route_authentication_setting) { { job_token_allowed: true } }
-
subject { find_user_from_job_token }
- context 'when the job token is in the headers' do
- it 'returns the user if valid job token' do
- set_header(described_class::JOB_TOKEN_HEADER, job.token)
-
- is_expected.to eq(user)
- expect(@current_authenticated_job).to eq(job)
- end
-
- it 'returns nil without job token' do
- set_header(described_class::JOB_TOKEN_HEADER, '')
-
- is_expected.to be_nil
- end
-
- it 'returns exception if invalid job token' do
- set_header(described_class::JOB_TOKEN_HEADER, 'invalid token')
-
- expect { subject }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ context 'when the token is in the headers' do
+ before do
+ set_header(described_class::JOB_TOKEN_HEADER, token)
end
- it 'returns exception if the job is not running' do
- set_header(described_class::JOB_TOKEN_HEADER, job.token)
- job.success!
+ it_behaves_like 'find user from job token'
+ end
- expect { subject }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ context 'when the token is in the job_token param' do
+ before do
+ set_param(described_class::JOB_TOKEN_PARAM, token)
end
- context 'when route is not allowed to be authenticated' do
- let(:route_authentication_setting) { { job_token_allowed: false } }
-
- it 'sets current_user to nil' do
- set_header(described_class::JOB_TOKEN_HEADER, job.token)
-
- allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(true)
-
- is_expected.to be_nil
- end
- end
+ it_behaves_like 'find user from job token'
end
- context 'when the job token is in the params' do
- shared_examples 'job token params' do |token_key_name|
- before do
- set_param(token_key_name, token)
- end
-
- context 'with valid job token' do
- let(:token) { job.token }
-
- it 'returns the user' do
- is_expected.to eq(user)
- expect(@current_authenticated_job).to eq(job)
- end
- end
-
- context 'with empty job token' do
- let(:token) { '' }
-
- it 'returns nil' do
- is_expected.to be_nil
- end
- end
-
- context 'with invalid job token' do
- let(:token) { 'invalid token' }
-
- it 'returns exception' do
- expect { subject }.to raise_error(Gitlab::Auth::UnauthorizedError)
- end
- end
-
- context 'when route is not allowed to be authenticated' do
- let(:route_authentication_setting) { { job_token_allowed: false } }
- let(:token) { job.token }
-
- it 'sets current_user to nil' do
- allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(true)
-
- is_expected.to be_nil
- end
- end
+ context 'when the token is in the token param' do
+ before do
+ set_param(described_class::RUNNER_JOB_TOKEN_PARAM, token)
end
- it_behaves_like 'job token params', described_class::JOB_TOKEN_PARAM
- it_behaves_like 'job token params', described_class::RUNNER_JOB_TOKEN_PARAM
+ it_behaves_like 'find user from job token'
end
context 'when the job token is provided via basic auth' do
@@ -834,7 +789,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
describe '#cluster_agent_token_from_authorization_token' do
- let_it_be(:agent_token) { create(:cluster_agent_token) }
+ let_it_be(:agent_token, freeze: true) { create(:cluster_agent_token) }
context 'when route_setting is empty' do
it 'returns nil' do
@@ -884,7 +839,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
describe '#find_runner_from_token' do
- let_it_be(:runner) { create(:ci_runner) }
+ let_it_be(:runner, freeze: true) { create(:ci_runner) }
context 'with API requests' do
before do
diff --git a/spec/lib/gitlab/auth/ip_rate_limiter_spec.rb b/spec/lib/gitlab/auth/ip_rate_limiter_spec.rb
index 3d782272d7e..f23fdd3fbcb 100644
--- a/spec/lib/gitlab/auth/ip_rate_limiter_spec.rb
+++ b/spec/lib/gitlab/auth/ip_rate_limiter_spec.rb
@@ -19,6 +19,9 @@ RSpec.describe Gitlab::Auth::IpRateLimiter, :use_clean_rails_memory_store_cachin
before do
stub_rack_attack_setting(options)
+ Rack::Attack.reset!
+ Rack::Attack.clear_configuration
+ Gitlab::RackAttack.configure(Rack::Attack)
end
after do
diff --git a/spec/lib/gitlab/auth/otp/session_enforcer_spec.rb b/spec/lib/gitlab/auth/otp/session_enforcer_spec.rb
deleted file mode 100644
index 928aade4008..00000000000
--- a/spec/lib/gitlab/auth/otp/session_enforcer_spec.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Auth::Otp::SessionEnforcer, :clean_gitlab_redis_shared_state do
- let_it_be(:key) { create(:key)}
-
- describe '#update_session' do
- it 'registers a session in Redis' do
- redis = double(:redis)
- expect(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis)
-
- expect(redis).to(
- receive(:setex)
- .with("#{described_class::OTP_SESSIONS_NAMESPACE}:#{key.id}",
- described_class::DEFAULT_EXPIRATION,
- true)
- .once)
-
- described_class.new(key).update_session
- end
- end
-
- describe '#access_restricted?' do
- subject { described_class.new(key).access_restricted? }
-
- context 'with existing session' do
- before do
- Gitlab::Redis::SharedState.with do |redis|
- redis.set("#{described_class::OTP_SESSIONS_NAMESPACE}:#{key.id}", true )
- end
- end
-
- it { is_expected.to be_falsey }
- end
-
- context 'without an existing session' do
- it { is_expected.to be_truthy }
- end
- end
-end
diff --git a/spec/lib/gitlab/auth/u2f_webauthn_converter_spec.rb b/spec/lib/gitlab/auth/u2f_webauthn_converter_spec.rb
new file mode 100644
index 00000000000..deddc7f5294
--- /dev/null
+++ b/spec/lib/gitlab/auth/u2f_webauthn_converter_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Auth::U2fWebauthnConverter do
+ let_it_be(:u2f_registration) do
+ device = U2F::FakeU2F.new(FFaker::BaconIpsum.characters(5))
+ create(:u2f_registration, name: 'u2f_device',
+ certificate: Base64.strict_encode64(device.cert_raw),
+ key_handle: U2F.urlsafe_encode64(device.key_handle_raw),
+ public_key: Base64.strict_encode64(device.origin_public_key_raw))
+ end
+
+ it 'converts u2f registration' do
+ webauthn_credential = WebAuthn::U2fMigrator.new(
+ app_id: Gitlab.config.gitlab.url,
+ certificate: u2f_registration.certificate,
+ key_handle: u2f_registration.key_handle,
+ public_key: u2f_registration.public_key,
+ counter: u2f_registration.counter
+ ).credential
+
+ converted_webauthn = described_class.new(u2f_registration).convert
+
+ expect(converted_webauthn).to(
+ include(user_id: u2f_registration.user_id,
+ credential_xid: Base64.strict_encode64(webauthn_credential.id)))
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/backfill_project_updated_at_after_repository_storage_move_spec.rb b/spec/lib/gitlab/background_migration/backfill_project_updated_at_after_repository_storage_move_spec.rb
new file mode 100644
index 00000000000..708e5e21dbe
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_project_updated_at_after_repository_storage_move_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BackfillProjectUpdatedAtAfterRepositoryStorageMove, :migration, schema: 20210210093901 do
+ let(:projects) { table(:projects) }
+ let(:project_repository_storage_moves) { table(:project_repository_storage_moves) }
+ let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+
+ subject { described_class.new }
+
+ describe '#perform' do
+ it 'updates project updated_at column if they were moved to a different repository storage' do
+ freeze_time do
+ project_1 = projects.create!(id: 1, namespace_id: namespace.id, updated_at: 1.day.ago)
+ project_2 = projects.create!(id: 2, namespace_id: namespace.id, updated_at: Time.current)
+ original_project_3_updated_at = 2.minutes.from_now
+ project_3 = projects.create!(id: 3, namespace_id: namespace.id, updated_at: original_project_3_updated_at)
+ original_project_4_updated_at = 10.days.ago
+ project_4 = projects.create!(id: 4, namespace_id: namespace.id, updated_at: original_project_4_updated_at)
+
+ repository_storage_move_1 = project_repository_storage_moves.create!(project_id: project_1.id, updated_at: 2.hours.ago, source_storage_name: 'default', destination_storage_name: 'default')
+ repository_storage_move_2 = project_repository_storage_moves.create!(project_id: project_2.id, updated_at: Time.current, source_storage_name: 'default', destination_storage_name: 'default')
+ project_repository_storage_moves.create!(project_id: project_3.id, updated_at: Time.current, source_storage_name: 'default', destination_storage_name: 'default')
+
+ subject.perform([1, 2, 3, 4, non_existing_record_id])
+
+ expect(project_1.reload.updated_at).to eq(repository_storage_move_1.updated_at + 1.second)
+ expect(project_2.reload.updated_at).to eq(repository_storage_move_2.updated_at + 1.second)
+ expect(project_3.reload.updated_at).to eq(original_project_3_updated_at)
+ expect(project_4.reload.updated_at).to eq(original_project_4_updated_at)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/populate_issue_email_participants_spec.rb b/spec/lib/gitlab/background_migration/populate_issue_email_participants_spec.rb
new file mode 100644
index 00000000000..f724b007e01
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/populate_issue_email_participants_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::PopulateIssueEmailParticipants, schema: 20201128210234 do
+ let!(:namespace) { table(:namespaces).create!(name: 'namespace', path: 'namespace') }
+ let!(:project) { table(:projects).create!(id: 1, namespace_id: namespace.id) }
+ let!(:issue1) { table(:issues).create!(id: 1, project_id: project.id, service_desk_reply_to: "a@gitlab.com") }
+ let!(:issue2) { table(:issues).create!(id: 2, project_id: project.id, service_desk_reply_to: "b@gitlab.com") }
+ let(:issue_email_participants) { table(:issue_email_participants) }
+
+ describe '#perform' do
+ it 'migrates email addresses from service desk issues', :aggregate_failures do
+ expect { subject.perform(1, 2) }.to change { issue_email_participants.count }.by(2)
+
+ expect(issue_email_participants.find_by(issue_id: 1).email).to eq("a@gitlab.com")
+ expect(issue_email_participants.find_by(issue_id: 2).email).to eq("b@gitlab.com")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings_spec.rb b/spec/lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings_spec.rb
new file mode 100644
index 00000000000..47e1d4620cd
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings_spec.rb
@@ -0,0 +1,135 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::RemoveDuplicateVulnerabilitiesFindings do
+ let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+ let(:users) { table(:users) }
+ let(:user) { create_user! }
+ let(:project) { table(:projects).create!(id: 123, namespace_id: namespace.id) }
+ let(:scanners) { table(:vulnerability_scanners) }
+ let!(:scanner) { scanners.create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
+ let!(:scanner2) { scanners.create!(project_id: project.id, external_id: 'test 2', name: 'test scanner 2') }
+ let!(:scanner3) { scanners.create!(project_id: project.id, external_id: 'test 3', name: 'test scanner 3') }
+ let!(:unrelated_scanner) { scanners.create!(project_id: project.id, external_id: 'unreleated_scanner', name: 'unrelated scanner') }
+ let(:vulnerabilities) { table(:vulnerabilities) }
+ let(:vulnerability_findings) { table(:vulnerability_occurrences) }
+ let(:vulnerability_identifiers) { table(:vulnerability_identifiers) }
+ let(:vulnerability_identifier) do
+ vulnerability_identifiers.create!(
+ project_id: project.id,
+ external_type: 'vulnerability-identifier',
+ external_id: 'vulnerability-identifier',
+ fingerprint: '7e394d1b1eb461a7406d7b1e08f057a1cf11287a',
+ name: 'vulnerability identifier')
+ end
+
+ let!(:first_finding) do
+ create_finding!(
+ uuid: "test1",
+ vulnerability_id: nil,
+ report_type: 0,
+ location_fingerprint: '2bda3014914481791847d8eca38d1a8d13b6ad76',
+ primary_identifier_id: vulnerability_identifier.id,
+ scanner_id: scanner.id,
+ project_id: project.id
+ )
+ end
+
+ let!(:first_duplicate) do
+ create_finding!(
+ uuid: "test2",
+ vulnerability_id: nil,
+ report_type: 0,
+ location_fingerprint: '2bda3014914481791847d8eca38d1a8d13b6ad76',
+ primary_identifier_id: vulnerability_identifier.id,
+ scanner_id: scanner2.id,
+ project_id: project.id
+ )
+ end
+
+ let!(:second_duplicate) do
+ create_finding!(
+ uuid: "test3",
+ vulnerability_id: nil,
+ report_type: 0,
+ location_fingerprint: '2bda3014914481791847d8eca38d1a8d13b6ad76',
+ primary_identifier_id: vulnerability_identifier.id,
+ scanner_id: scanner3.id,
+ project_id: project.id
+ )
+ end
+
+ let!(:unrelated_finding) do
+ create_finding!(
+ uuid: "unreleated_finding",
+ vulnerability_id: nil,
+ report_type: 1,
+ location_fingerprint: 'random_location_fingerprint',
+ primary_identifier_id: vulnerability_identifier.id,
+ scanner_id: unrelated_scanner.id,
+ project_id: project.id
+ )
+ end
+
+ subject { described_class.new.perform(first_finding.id, unrelated_finding.id) }
+
+ before do
+ stub_const("#{described_class}::DELETE_BATCH_SIZE", 1)
+ end
+
+ it "removes entries which would result in duplicate UUIDv5" do
+ expect(vulnerability_findings.count).to eq(4)
+
+ expect { subject }.to change { vulnerability_findings.count }.from(4).to(2)
+
+ expect(vulnerability_findings.pluck(:id)).to eq([second_duplicate.id, unrelated_finding.id])
+ end
+
+ private
+
+ def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0)
+ vulnerabilities.create!(
+ project_id: project_id,
+ author_id: author_id,
+ title: title,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type
+ )
+ end
+
+ # rubocop:disable Metrics/ParameterLists
+ def create_finding!(
+ vulnerability_id:, project_id:, scanner_id:, primary_identifier_id:,
+ name: "test", severity: 7, confidence: 7, report_type: 0,
+ project_fingerprint: '123qweasdzxc', location_fingerprint: 'test',
+ metadata_version: 'test', raw_metadata: 'test', uuid: 'test')
+ vulnerability_findings.create!(
+ vulnerability_id: vulnerability_id,
+ project_id: project_id,
+ name: name,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type,
+ project_fingerprint: project_fingerprint,
+ scanner_id: scanner_id,
+ primary_identifier_id: vulnerability_identifier.id,
+ location_fingerprint: location_fingerprint,
+ metadata_version: metadata_version,
+ raw_metadata: raw_metadata,
+ uuid: uuid
+ )
+ end
+ # rubocop:enable Metrics/ParameterLists
+
+ def create_user!(name: "Example User", email: "user@example.com", user_type: nil, created_at: Time.zone.now, confirmed_at: Time.zone.now)
+ users.create!(
+ name: name,
+ email: email,
+ username: name,
+ projects_limit: 0,
+ user_type: user_type,
+ confirmed_at: confirmed_at
+ )
+ end
+end
diff --git a/spec/lib/gitlab/background_migration_spec.rb b/spec/lib/gitlab/background_migration_spec.rb
index 052a01a8dd8..5b20572578c 100644
--- a/spec/lib/gitlab/background_migration_spec.rb
+++ b/spec/lib/gitlab/background_migration_spec.rb
@@ -55,7 +55,7 @@ RSpec.describe Gitlab::BackgroundMigration do
expect(described_class).to receive(:perform)
.with('Foo', [10, 20])
- described_class.steal('Foo') { |(arg1, arg2)| arg1 == 10 && arg2 == 20 }
+ described_class.steal('Foo') { |job| job.args.second.first == 10 && job.args.second.second == 20 }
end
it 'does not steal jobs that do not match the predicate' do
diff --git a/spec/lib/gitlab/changelog/ast_spec.rb b/spec/lib/gitlab/changelog/ast_spec.rb
new file mode 100644
index 00000000000..fa15ac979fe
--- /dev/null
+++ b/spec/lib/gitlab/changelog/ast_spec.rb
@@ -0,0 +1,246 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Changelog::AST::Identifier do
+ let(:state) { Gitlab::Changelog::EvalState.new }
+
+ describe '#evaluate' do
+ it 'evaluates a selector' do
+ data = { 'number' => 10 }
+
+ expect(described_class.new('number').evaluate(state, data)).to eq(10)
+ end
+
+ it 'returns nil if the key is not set' do
+ expect(described_class.new('number').evaluate(state, {})).to be_nil
+ end
+
+ it 'returns nil if the input is not a Hash' do
+ expect(described_class.new('number').evaluate(state, 45)).to be_nil
+ end
+
+ it 'returns the current data when using the special identifier "it"' do
+ expect(described_class.new('it').evaluate(state, 45)).to eq(45)
+ end
+ end
+end
+
+RSpec.describe Gitlab::Changelog::AST::Integer do
+ let(:state) { Gitlab::Changelog::EvalState.new }
+
+ describe '#evaluate' do
+ it 'evaluates a selector' do
+ expect(described_class.new(0).evaluate(state, [10])).to eq(10)
+ end
+
+ it 'returns nil if the index is not set' do
+ expect(described_class.new(1).evaluate(state, [10])).to be_nil
+ end
+
+ it 'returns nil if the input is not an Array' do
+ expect(described_class.new(0).evaluate(state, {})).to be_nil
+ end
+ end
+end
+
+RSpec.describe Gitlab::Changelog::AST::Selector do
+ let(:state) { Gitlab::Changelog::EvalState.new }
+ let(:data) { { 'numbers' => [10] } }
+
+ describe '#evaluate' do
+ it 'evaluates a selector' do
+ ident = Gitlab::Changelog::AST::Identifier.new('numbers')
+ int = Gitlab::Changelog::AST::Integer.new(0)
+
+ expect(described_class.new([ident, int]).evaluate(state, data)).to eq(10)
+ end
+
+ it 'evaluates a selector that returns nil' do
+ int = Gitlab::Changelog::AST::Integer.new(0)
+
+ expect(described_class.new([int]).evaluate(state, data)).to be_nil
+ end
+ end
+end
+
+RSpec.describe Gitlab::Changelog::AST::Variable do
+ let(:state) { Gitlab::Changelog::EvalState.new }
+ let(:data) { { 'numbers' => [10] } }
+
+ describe '#evaluate' do
+ it 'evaluates a variable' do
+ node = Gitlab::Changelog::Parser
+ .new
+ .parse_and_transform('{{numbers.0}}')
+ .nodes[0]
+
+ expect(node.evaluate(state, data)).to eq('10')
+ end
+
+ it 'evaluates an undefined variable' do
+ node =
+ Gitlab::Changelog::Parser.new.parse_and_transform('{{foobar}}').nodes[0]
+
+ expect(node.evaluate(state, data)).to eq('')
+ end
+
+ it 'evaluates the special variable "it"' do
+ node =
+ Gitlab::Changelog::Parser.new.parse_and_transform('{{it}}').nodes[0]
+
+ expect(node.evaluate(state, data)).to eq(data.to_s)
+ end
+ end
+end
+
+RSpec.describe Gitlab::Changelog::AST::Expressions do
+ let(:state) { Gitlab::Changelog::EvalState.new }
+
+ describe '#evaluate' do
+ it 'evaluates all expressions' do
+ node = Gitlab::Changelog::Parser
+ .new
+ .parse_and_transform('{{number}}foo')
+
+ expect(node.evaluate(state, { 'number' => 10 })).to eq('10foo')
+ end
+ end
+end
+
+RSpec.describe Gitlab::Changelog::AST::Text do
+ let(:state) { Gitlab::Changelog::EvalState.new }
+
+ describe '#evaluate' do
+ it 'returns the text' do
+ expect(described_class.new('foo').evaluate(state, {})).to eq('foo')
+ end
+ end
+end
+
+RSpec.describe Gitlab::Changelog::AST::If do
+ let(:state) { Gitlab::Changelog::EvalState.new }
+
+ describe '#evaluate' do
+ it 'evaluates a truthy if expression without an else clause' do
+ node = Gitlab::Changelog::Parser
+ .new
+ .parse_and_transform('{% if thing %}foo{% end %}')
+ .nodes[0]
+
+ expect(node.evaluate(state, { 'thing' => true })).to eq('foo')
+ end
+
+ it 'evaluates a falsy if expression without an else clause' do
+ node = Gitlab::Changelog::Parser
+ .new
+ .parse_and_transform('{% if thing %}foo{% end %}')
+ .nodes[0]
+
+ expect(node.evaluate(state, { 'thing' => false })).to eq('')
+ end
+
+ it 'evaluates a falsy if expression with an else clause' do
+ node = Gitlab::Changelog::Parser
+ .new
+ .parse_and_transform('{% if thing %}foo{% else %}bar{% end %}')
+ .nodes[0]
+
+ expect(node.evaluate(state, { 'thing' => false })).to eq('bar')
+ end
+ end
+
+ describe '#truthy?' do
+ it 'returns true for a non-empty String' do
+ expect(described_class.new.truthy?('foo')).to eq(true)
+ end
+
+ it 'returns true for a non-empty Array' do
+ expect(described_class.new.truthy?([10])).to eq(true)
+ end
+
+ it 'returns true for a Boolean true' do
+ expect(described_class.new.truthy?(true)).to eq(true)
+ end
+
+ it 'returns false for an empty String' do
+ expect(described_class.new.truthy?('')).to eq(false)
+ end
+
+ it 'returns true for an empty Array' do
+ expect(described_class.new.truthy?([])).to eq(false)
+ end
+
+ it 'returns false for a Boolean false' do
+ expect(described_class.new.truthy?(false)).to eq(false)
+ end
+ end
+end
+
+RSpec.describe Gitlab::Changelog::AST::Each do
+ let(:state) { Gitlab::Changelog::EvalState.new }
+
+ describe '#evaluate' do
+ it 'evaluates the expression' do
+ data = { 'animals' => [{ 'name' => 'Cat' }, { 'name' => 'Dog' }] }
+ node = Gitlab::Changelog::Parser
+ .new
+ .parse_and_transform('{% each animals %}{{name}}{% end %}')
+ .nodes[0]
+
+ expect(node.evaluate(state, data)).to eq('CatDog')
+ end
+
+ it 'returns an empty string when the input is not a collection' do
+ data = { 'animals' => 10 }
+ node = Gitlab::Changelog::Parser
+ .new
+ .parse_and_transform('{% each animals %}{{name}}{% end %}')
+ .nodes[0]
+
+ expect(node.evaluate(state, data)).to eq('')
+ end
+
+ it 'disallows too many nested loops' do
+ data = {
+ 'foo' => [
+ {
+ 'bar' => [
+ {
+ 'baz' => [
+ {
+ 'quix' => [
+ {
+ 'foo' => [{ 'name' => 'Alice' }]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+
+ template = <<~TPL
+ {% each foo %}
+ {% each bar %}
+ {% each baz %}
+ {% each quix %}
+ {% each foo %}
+ {{name}}
+ {% end %}
+ {% end %}
+ {% end %}
+ {% end %}
+ {% end %}
+ TPL
+
+ node =
+ Gitlab::Changelog::Parser.new.parse_and_transform(template).nodes[0]
+
+ expect { node.evaluate(state, data) }
+ .to raise_error(Gitlab::Changelog::Error)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/changelog/committer_spec.rb b/spec/lib/gitlab/changelog/committer_spec.rb
new file mode 100644
index 00000000000..1e04fe346cb
--- /dev/null
+++ b/spec/lib/gitlab/changelog/committer_spec.rb
@@ -0,0 +1,128 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Changelog::Committer do
+ let(:project) { create(:project, :repository) }
+ let(:user) { project.creator }
+ let(:committer) { described_class.new(project, user) }
+ let(:config) { Gitlab::Changelog::Config.new(project) }
+
+ describe '#commit' do
+ context "when the release isn't in the changelog" do
+ it 'commits the changes' do
+ release = Gitlab::Changelog::Release
+ .new(version: '1.0.0', date: Time.utc(2020, 1, 1), config: config)
+
+ committer.commit(
+ release: release,
+ file: 'CHANGELOG.md',
+ branch: 'master',
+ message: 'Test commit'
+ )
+
+ content = project.repository.blob_at('master', 'CHANGELOG.md').data
+
+ expect(content).to eq(<<~MARKDOWN)
+ ## 1.0.0 (2020-01-01)
+
+ No changes.
+ MARKDOWN
+ end
+ end
+
+ context 'when the release is already in the changelog' do
+ it "doesn't commit the changes" do
+ release = Gitlab::Changelog::Release
+ .new(version: '1.0.0', date: Time.utc(2020, 1, 1), config: config)
+
+ 2.times do
+ committer.commit(
+ release: release,
+ file: 'CHANGELOG.md',
+ branch: 'master',
+ message: 'Test commit'
+ )
+ end
+
+ content = project.repository.blob_at('master', 'CHANGELOG.md').data
+
+ expect(content).to eq(<<~MARKDOWN)
+ ## 1.0.0 (2020-01-01)
+
+ No changes.
+ MARKDOWN
+ end
+ end
+
+ context 'when committing the changes fails' do
+ it 'retries the operation' do
+ release = Gitlab::Changelog::Release
+ .new(version: '1.0.0', date: Time.utc(2020, 1, 1), config: config)
+
+ service = instance_spy(Files::MultiService)
+ errored = false
+
+ allow(Files::MultiService)
+ .to receive(:new)
+ .and_return(service)
+
+ allow(service).to receive(:execute) do
+ if errored
+ { status: :success }
+ else
+ errored = true
+ { status: :error }
+ end
+ end
+
+ expect do
+ committer.commit(
+ release: release,
+ file: 'CHANGELOG.md',
+ branch: 'master',
+ message: 'Test commit'
+ )
+ end.not_to raise_error
+ end
+ end
+
+ context "when the changelog changes before saving the changes" do
+ it 'raises a Error' do
+ release1 = Gitlab::Changelog::Release
+ .new(version: '1.0.0', date: Time.utc(2020, 1, 1), config: config)
+
+ release2 = Gitlab::Changelog::Release
+ .new(version: '2.0.0', date: Time.utc(2020, 1, 1), config: config)
+
+ # This creates the initial commit we'll later use to see if the
+ # changelog changed before saving our changes.
+ committer.commit(
+ release: release1,
+ file: 'CHANGELOG.md',
+ branch: 'master',
+ message: 'Initial commit'
+ )
+
+ allow(Gitlab::Git::Commit)
+ .to receive(:last_for_path)
+ .with(
+ project.repository,
+ 'master',
+ 'CHANGELOG.md',
+ literal_pathspec: true
+ )
+ .and_return(double(:commit, sha: 'foo'))
+
+ expect do
+ committer.commit(
+ release: release2,
+ file: 'CHANGELOG.md',
+ branch: 'master',
+ message: 'Test commit'
+ )
+ end.to raise_error(Gitlab::Changelog::Error)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/changelog/config_spec.rb b/spec/lib/gitlab/changelog/config_spec.rb
new file mode 100644
index 00000000000..51988acf3d1
--- /dev/null
+++ b/spec/lib/gitlab/changelog/config_spec.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Changelog::Config do
+ let(:project) { build_stubbed(:project) }
+
+ describe '.from_git' do
+ it 'retrieves the configuration from Git' do
+ allow(project.repository)
+ .to receive(:changelog_config)
+ .and_return("---\ndate_format: '%Y'")
+
+ expect(described_class)
+ .to receive(:from_hash)
+ .with(project, 'date_format' => '%Y')
+
+ described_class.from_git(project)
+ end
+
+ it 'returns the default configuration when no YAML file exists in Git' do
+ allow(project.repository)
+ .to receive(:changelog_config)
+ .and_return(nil)
+
+ expect(described_class)
+ .to receive(:new)
+ .with(project)
+
+ described_class.from_git(project)
+ end
+ end
+
+ describe '.from_hash' do
+ it 'sets the configuration according to a Hash' do
+ config = described_class.from_hash(
+ project,
+ 'date_format' => 'foo',
+ 'template' => 'bar',
+ 'categories' => { 'foo' => 'bar' }
+ )
+
+ expect(config.date_format).to eq('foo')
+ expect(config.template)
+ .to be_instance_of(Gitlab::Changelog::AST::Expressions)
+
+ expect(config.categories).to eq({ 'foo' => 'bar' })
+ end
+
+ it 'raises Error when the categories are not a Hash' do
+ expect { described_class.from_hash(project, 'categories' => 10) }
+ .to raise_error(Gitlab::Changelog::Error)
+ end
+ end
+
+ describe '#contributor?' do
+ it 'returns true if a user is a contributor' do
+ user = build_stubbed(:author)
+
+ allow(project.team).to receive(:contributor?).with(user).and_return(true)
+
+ expect(described_class.new(project).contributor?(user)).to eq(true)
+ end
+
+ it "returns true if a user isn't a contributor" do
+ user = build_stubbed(:author)
+
+ allow(project.team).to receive(:contributor?).with(user).and_return(false)
+
+ expect(described_class.new(project).contributor?(user)).to eq(false)
+ end
+ end
+
+ describe '#category' do
+ it 'returns the name of a category' do
+ config = described_class.new(project)
+
+ config.categories['foo'] = 'Foo'
+
+ expect(config.category('foo')).to eq('Foo')
+ end
+
+ it 'returns the raw category name when no alternative name is configured' do
+ config = described_class.new(project)
+
+ expect(config.category('bla')).to eq('bla')
+ end
+ end
+
+ describe '#format_date' do
+ it 'formats a date according to the configured date format' do
+ config = described_class.new(project)
+ time = Time.utc(2021, 1, 5)
+
+ expect(config.format_date(time)).to eq('2021-01-05')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/changelog/generator_spec.rb b/spec/lib/gitlab/changelog/generator_spec.rb
new file mode 100644
index 00000000000..bc4a7c5dd6b
--- /dev/null
+++ b/spec/lib/gitlab/changelog/generator_spec.rb
@@ -0,0 +1,164 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Changelog::Generator do
+ describe '#add' do
+ let(:project) { build_stubbed(:project) }
+ let(:author) { build_stubbed(:user) }
+ let(:commit) { build_stubbed(:commit) }
+ let(:config) { Gitlab::Changelog::Config.new(project) }
+
+ it 'generates the Markdown for the first release' do
+ release = Gitlab::Changelog::Release.new(
+ version: '1.0.0',
+ date: Time.utc(2021, 1, 5),
+ config: config
+ )
+
+ release.add_entry(
+ title: 'This is a new change',
+ commit: commit,
+ category: 'added',
+ author: author
+ )
+
+ gen = described_class.new('')
+
+ expect(gen.add(release)).to eq(<<~MARKDOWN)
+ ## 1.0.0 (2021-01-05)
+
+ ### added (1 change)
+
+ - [This is a new change](#{commit.to_reference(full: true)})
+ MARKDOWN
+ end
+
+ it 'generates the Markdown for a newer release' do
+ release = Gitlab::Changelog::Release.new(
+ version: '2.0.0',
+ date: Time.utc(2021, 1, 5),
+ config: config
+ )
+
+ release.add_entry(
+ title: 'This is a new change',
+ commit: commit,
+ category: 'added',
+ author: author
+ )
+
+ gen = described_class.new(<<~MARKDOWN)
+ This is a changelog file.
+
+ ## 1.0.0
+
+ This is the changelog for version 1.0.0.
+ MARKDOWN
+
+ expect(gen.add(release)).to eq(<<~MARKDOWN)
+ This is a changelog file.
+
+ ## 2.0.0 (2021-01-05)
+
+ ### added (1 change)
+
+ - [This is a new change](#{commit.to_reference(full: true)})
+
+ ## 1.0.0
+
+ This is the changelog for version 1.0.0.
+ MARKDOWN
+ end
+
+ it 'generates the Markdown for a patch release' do
+ release = Gitlab::Changelog::Release.new(
+ version: '1.1.0',
+ date: Time.utc(2021, 1, 5),
+ config: config
+ )
+
+ release.add_entry(
+ title: 'This is a new change',
+ commit: commit,
+ category: 'added',
+ author: author
+ )
+
+ gen = described_class.new(<<~MARKDOWN)
+ This is a changelog file.
+
+ ## 2.0.0
+
+ This is another release.
+
+ ## 1.0.0
+
+ This is the changelog for version 1.0.0.
+ MARKDOWN
+
+ expect(gen.add(release)).to eq(<<~MARKDOWN)
+ This is a changelog file.
+
+ ## 2.0.0
+
+ This is another release.
+
+ ## 1.1.0 (2021-01-05)
+
+ ### added (1 change)
+
+ - [This is a new change](#{commit.to_reference(full: true)})
+
+ ## 1.0.0
+
+ This is the changelog for version 1.0.0.
+ MARKDOWN
+ end
+
+ it 'generates the Markdown for an old release' do
+ release = Gitlab::Changelog::Release.new(
+ version: '0.5.0',
+ date: Time.utc(2021, 1, 5),
+ config: config
+ )
+
+ release.add_entry(
+ title: 'This is a new change',
+ commit: commit,
+ category: 'added',
+ author: author
+ )
+
+ gen = described_class.new(<<~MARKDOWN)
+ This is a changelog file.
+
+ ## 2.0.0
+
+ This is another release.
+
+ ## 1.0.0
+
+ This is the changelog for version 1.0.0.
+ MARKDOWN
+
+ expect(gen.add(release)).to eq(<<~MARKDOWN)
+ This is a changelog file.
+
+ ## 2.0.0
+
+ This is another release.
+
+ ## 1.0.0
+
+ This is the changelog for version 1.0.0.
+
+ ## 0.5.0 (2021-01-05)
+
+ ### added (1 change)
+
+ - [This is a new change](#{commit.to_reference(full: true)})
+ MARKDOWN
+ end
+ end
+end
diff --git a/spec/lib/gitlab/changelog/parser_spec.rb b/spec/lib/gitlab/changelog/parser_spec.rb
new file mode 100644
index 00000000000..1d353f5eb35
--- /dev/null
+++ b/spec/lib/gitlab/changelog/parser_spec.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Changelog::Parser do
+ let(:parser) { described_class.new }
+
+ describe '#root' do
+ it 'parses an empty template' do
+ expect(parser.root).to parse('')
+ end
+
+ it 'parses a variable with a single identifier step' do
+ expect(parser.root).to parse('{{foo}}')
+ end
+
+ it 'parses a variable with a single integer step' do
+ expect(parser.root).to parse('{{0}}')
+ end
+
+ it 'parses a variable with multiple selector steps' do
+ expect(parser.root).to parse('{{foo.bar}}')
+ end
+
+ it 'parses a variable with an integer selector step' do
+ expect(parser.root).to parse('{{foo.bar.0}}')
+ end
+
+ it 'parses the special "it" variable' do
+ expect(parser.root).to parse('{{it}}')
+ end
+
+ it 'parses a text node' do
+ expect(parser.root).to parse('foo')
+ end
+
+ it 'parses an if expression' do
+ expect(parser.root).to parse('{% if foo %}bar{% end %}')
+ end
+
+ it 'parses an if-else expression' do
+ expect(parser.root).to parse('{% if foo %}bar{% else %}baz{% end %}')
+ end
+
+ it 'parses an each expression' do
+ expect(parser.root).to parse('{% each foo %}foo{% end %}')
+ end
+
+ it 'parses an escaped newline' do
+ expect(parser.root).to parse("foo\\\nbar")
+ end
+
+ it 'parses a regular newline' do
+ expect(parser.root).to parse("foo\nbar")
+ end
+
+ it 'parses the default changelog template' do
+ expect(parser.root).to parse(Gitlab::Changelog::Config::DEFAULT_TEMPLATE)
+ end
+
+ it 'raises an error when parsing an integer selector that is too large' do
+ expect(parser.root).not_to parse('{{100000000000}}')
+ end
+ end
+
+ describe '#parse_and_transform' do
+ it 'parses and transforms a template' do
+ node = parser.parse_and_transform('foo')
+
+ expect(node).to be_instance_of(Gitlab::Changelog::AST::Expressions)
+ end
+
+ it 'raises parsing errors using a custom error class' do
+ expect { parser.parse_and_transform('{% each') }
+ .to raise_error(Gitlab::Changelog::Error)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/changelog/release_spec.rb b/spec/lib/gitlab/changelog/release_spec.rb
new file mode 100644
index 00000000000..f95244d6750
--- /dev/null
+++ b/spec/lib/gitlab/changelog/release_spec.rb
@@ -0,0 +1,129 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Changelog::Release do
+ describe '#to_markdown' do
+ let(:config) { Gitlab::Changelog::Config.new(build_stubbed(:project)) }
+ let(:commit) { build_stubbed(:commit) }
+ let(:author) { build_stubbed(:user) }
+ let(:mr) { build_stubbed(:merge_request) }
+ let(:release) do
+ described_class
+ .new(version: '1.0.0', date: Time.utc(2021, 1, 5), config: config)
+ end
+
+ context 'when there are no entries' do
+ it 'includes a notice about the lack of entries' do
+ expect(release.to_markdown).to eq(<<~OUT)
+ ## 1.0.0 (2021-01-05)
+
+ No changes.
+
+ OUT
+ end
+ end
+
+ context 'when all data is present' do
+ it 'includes all data' do
+ allow(config).to receive(:contributor?).with(author).and_return(true)
+
+ release.add_entry(
+ title: 'Entry title',
+ commit: commit,
+ category: 'fixed',
+ author: author,
+ merge_request: mr
+ )
+
+ expect(release.to_markdown).to eq(<<~OUT)
+ ## 1.0.0 (2021-01-05)
+
+ ### fixed (1 change)
+
+ - [Entry title](#{commit.to_reference(full: true)}) \
+ by #{author.to_reference(full: true)} \
+ ([merge request](#{mr.to_reference(full: true)}))
+
+ OUT
+ end
+ end
+
+ context 'when no merge request is present' do
+ it "doesn't include a merge request link" do
+ allow(config).to receive(:contributor?).with(author).and_return(true)
+
+ release.add_entry(
+ title: 'Entry title',
+ commit: commit,
+ category: 'fixed',
+ author: author
+ )
+
+ expect(release.to_markdown).to eq(<<~OUT)
+ ## 1.0.0 (2021-01-05)
+
+ ### fixed (1 change)
+
+ - [Entry title](#{commit.to_reference(full: true)}) \
+ by #{author.to_reference(full: true)}
+
+ OUT
+ end
+ end
+
+ context 'when the author is not a contributor' do
+ it "doesn't include the author" do
+ allow(config).to receive(:contributor?).with(author).and_return(false)
+
+ release.add_entry(
+ title: 'Entry title',
+ commit: commit,
+ category: 'fixed',
+ author: author
+ )
+
+ expect(release.to_markdown).to eq(<<~OUT)
+ ## 1.0.0 (2021-01-05)
+
+ ### fixed (1 change)
+
+ - [Entry title](#{commit.to_reference(full: true)})
+
+ OUT
+ end
+ end
+
+ context 'when a category has no entries' do
+ it "isn't included in the output" do
+ config.categories['kittens'] = 'Kittens'
+ config.categories['fixed'] = 'Bug fixes'
+
+ release.add_entry(
+ title: 'Entry title',
+ commit: commit,
+ category: 'fixed'
+ )
+
+ expect(release.to_markdown).to eq(<<~OUT)
+ ## 1.0.0 (2021-01-05)
+
+ ### Bug fixes (1 change)
+
+ - [Entry title](#{commit.to_reference(full: true)})
+
+ OUT
+ end
+ end
+ end
+
+ describe '#header_start_position' do
+ it 'returns a regular expression for finding the start of a release section' do
+ config = Gitlab::Changelog::Config.new(build_stubbed(:project))
+ release = described_class
+ .new(version: '1.0.0', date: Time.utc(2021, 1, 5), config: config)
+
+ expect(release.header_start_pattern).to eq(/^##\s*1\.0\.0/)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/badge/coverage/metadata_spec.rb b/spec/lib/gitlab/ci/badge/coverage/metadata_spec.rb
index 725ae03ad74..6d272f060ab 100644
--- a/spec/lib/gitlab/badge/coverage/metadata_spec.rb
+++ b/spec/lib/gitlab/ci/badge/coverage/metadata_spec.rb
@@ -1,9 +1,9 @@
# frozen_string_literal: true
require 'spec_helper'
-require 'lib/gitlab/badge/shared/metadata'
+require 'lib/gitlab/ci/badge/shared/metadata'
-RSpec.describe Gitlab::Badge::Coverage::Metadata do
+RSpec.describe Gitlab::Ci::Badge::Coverage::Metadata do
let(:badge) do
double(project: create(:project), ref: 'feature', job: 'test')
end
diff --git a/spec/lib/gitlab/badge/coverage/report_spec.rb b/spec/lib/gitlab/ci/badge/coverage/report_spec.rb
index 3b5ea3291e4..13696d815aa 100644
--- a/spec/lib/gitlab/badge/coverage/report_spec.rb
+++ b/spec/lib/gitlab/ci/badge/coverage/report_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Badge::Coverage::Report do
+RSpec.describe Gitlab::Ci::Badge::Coverage::Report do
let_it_be(:project) { create(:project) }
let_it_be(:success_pipeline) { create(:ci_pipeline, :success, project: project) }
let_it_be(:running_pipeline) { create(:ci_pipeline, :running, project: project) }
diff --git a/spec/lib/gitlab/badge/coverage/template_spec.rb b/spec/lib/gitlab/ci/badge/coverage/template_spec.rb
index ba5c1b2ce6e..f010d1bce50 100644
--- a/spec/lib/gitlab/badge/coverage/template_spec.rb
+++ b/spec/lib/gitlab/ci/badge/coverage/template_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Badge::Coverage::Template do
+RSpec.describe Gitlab::Ci::Badge::Coverage::Template do
let(:badge) { double(entity: 'coverage', status: 90.00, customization: {}) }
let(:template) { described_class.new(badge) }
diff --git a/spec/lib/gitlab/badge/pipeline/metadata_spec.rb b/spec/lib/gitlab/ci/badge/pipeline/metadata_spec.rb
index c8ed0c8ea29..2f677237fad 100644
--- a/spec/lib/gitlab/badge/pipeline/metadata_spec.rb
+++ b/spec/lib/gitlab/ci/badge/pipeline/metadata_spec.rb
@@ -1,9 +1,9 @@
# frozen_string_literal: true
require 'spec_helper'
-require 'lib/gitlab/badge/shared/metadata'
+require 'lib/gitlab/ci/badge/shared/metadata'
-RSpec.describe Gitlab::Badge::Pipeline::Metadata do
+RSpec.describe Gitlab::Ci::Badge::Pipeline::Metadata do
let(:badge) { double(project: create(:project), ref: 'feature') }
let(:metadata) { described_class.new(badge) }
diff --git a/spec/lib/gitlab/badge/pipeline/status_spec.rb b/spec/lib/gitlab/ci/badge/pipeline/status_spec.rb
index b5dabca0477..45d0d781090 100644
--- a/spec/lib/gitlab/badge/pipeline/status_spec.rb
+++ b/spec/lib/gitlab/ci/badge/pipeline/status_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Badge::Pipeline::Status do
+RSpec.describe Gitlab::Ci::Badge::Pipeline::Status do
let(:project) { create(:project, :repository) }
let(:sha) { project.commit.sha }
let(:branch) { 'master' }
diff --git a/spec/lib/gitlab/badge/pipeline/template_spec.rb b/spec/lib/gitlab/ci/badge/pipeline/template_spec.rb
index c78e95852f3..696bb62b4d6 100644
--- a/spec/lib/gitlab/badge/pipeline/template_spec.rb
+++ b/spec/lib/gitlab/ci/badge/pipeline/template_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Badge::Pipeline::Template do
+RSpec.describe Gitlab::Ci::Badge::Pipeline::Template do
let(:badge) { double(entity: 'pipeline', status: 'success', customization: {}) }
let(:template) { described_class.new(badge) }
diff --git a/spec/lib/gitlab/badge/shared/metadata.rb b/spec/lib/gitlab/ci/badge/shared/metadata.rb
index c99a65bb2f4..c99a65bb2f4 100644
--- a/spec/lib/gitlab/badge/shared/metadata.rb
+++ b/spec/lib/gitlab/ci/badge/shared/metadata.rb
diff --git a/spec/lib/gitlab/ci/build/credentials/registry/dependency_proxy_spec.rb b/spec/lib/gitlab/ci/build/credentials/registry/dependency_proxy_spec.rb
new file mode 100644
index 00000000000..f50c6e99e99
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/credentials/registry/dependency_proxy_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Build::Credentials::Registry::DependencyProxy do
+ let(:build) { create(:ci_build, name: 'spinach', stage: 'test', stage_idx: 0) }
+ let(:gitlab_url) { 'gitlab.example.com:443' }
+
+ subject { described_class.new(build) }
+
+ before do
+ stub_config_setting(host: 'gitlab.example.com', port: 443)
+ end
+
+ it 'contains valid dependency proxy credentials' do
+ expect(subject).to be_kind_of(described_class)
+
+ expect(subject.username).to eq 'gitlab-ci-token'
+ expect(subject.password).to eq build.token
+ expect(subject.url).to eq gitlab_url
+ expect(subject.type).to eq 'registry'
+ end
+
+ describe '.valid?' do
+ subject { described_class.new(build).valid? }
+
+ context 'when dependency proxy is enabled' do
+ before do
+ stub_config(dependency_proxy: { enabled: true })
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when dependency proxy is disabled' do
+ before do
+ stub_config(dependency_proxy: { enabled: false })
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/build/credentials/registry_spec.rb b/spec/lib/gitlab/ci/build/credentials/registry/gitlab_registry_spec.rb
index c0a76973f60..43913e91085 100644
--- a/spec/lib/gitlab/ci/build/credentials/registry_spec.rb
+++ b/spec/lib/gitlab/ci/build/credentials/registry/gitlab_registry_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Build::Credentials::Registry do
+RSpec.describe Gitlab::Ci::Build::Credentials::Registry::GitlabRegistry do
let(:build) { create(:ci_build, name: 'spinach', stage: 'test', stage_idx: 0) }
let(:registry_url) { 'registry.example.com:5005' }
diff --git a/spec/lib/gitlab/ci/build/rules_spec.rb b/spec/lib/gitlab/ci/build/rules_spec.rb
index a1af5b75f87..0b50def05d4 100644
--- a/spec/lib/gitlab/ci/build/rules_spec.rb
+++ b/spec/lib/gitlab/ci/build/rules_spec.rb
@@ -201,40 +201,13 @@ RSpec.describe Gitlab::Ci::Build::Rules do
end
describe '#build_attributes' do
- let(:seed_attributes) { {} }
-
subject(:build_attributes) do
- result.build_attributes(seed_attributes)
+ result.build_attributes
end
it 'compacts nil values' do
is_expected.to eq(options: {}, when: 'on_success')
end
-
- context 'when there are variables in rules' do
- let(:variables) { { VAR1: 'new var 1', VAR3: 'var 3' } }
-
- context 'when there are seed variables' do
- let(:seed_attributes) do
- { yaml_variables: [{ key: 'VAR1', value: 'var 1', public: true },
- { key: 'VAR2', value: 'var 2', public: true }] }
- end
-
- it 'returns yaml_variables with override' do
- is_expected.to include(
- yaml_variables: [{ key: 'VAR1', value: 'new var 1', public: true },
- { key: 'VAR2', value: 'var 2', public: true },
- { key: 'VAR3', value: 'var 3', public: true }]
- )
- end
- end
-
- context 'when there is not seed variables' do
- it 'does not return yaml_variables' do
- is_expected.not_to have_key(:yaml_variables)
- end
- end
- end
end
describe '#pass?' do
diff --git a/spec/lib/gitlab/ci/charts_spec.rb b/spec/lib/gitlab/ci/charts_spec.rb
index cfc2019a89b..46d7d4a58f0 100644
--- a/spec/lib/gitlab/ci/charts_spec.rb
+++ b/spec/lib/gitlab/ci/charts_spec.rb
@@ -9,6 +9,10 @@ RSpec.describe Gitlab::Ci::Charts do
subject { chart.to }
+ before do
+ create(:ci_empty_pipeline, project: project, duration: 120)
+ end
+
it 'goes until the end of the current month (including the whole last day of the month)' do
is_expected.to eq(Date.today.end_of_month.end_of_day)
end
@@ -20,6 +24,10 @@ RSpec.describe Gitlab::Ci::Charts do
it 'uses %B %Y as labels format' do
expect(chart.labels).to include(chart.from.strftime('%B %Y'))
end
+
+ it 'returns count of pipelines run each day in the current year' do
+ expect(chart.total.sum).to eq(1)
+ end
end
context 'monthchart' do
@@ -28,6 +36,10 @@ RSpec.describe Gitlab::Ci::Charts do
subject { chart.to }
+ before do
+ create(:ci_empty_pipeline, project: project, duration: 120)
+ end
+
it 'includes the whole current day' do
is_expected.to eq(Date.today.end_of_day)
end
@@ -39,6 +51,10 @@ RSpec.describe Gitlab::Ci::Charts do
it 'uses %d %B as labels format' do
expect(chart.labels).to include(chart.from.strftime('%d %B'))
end
+
+ it 'returns count of pipelines run each day in the current month' do
+ expect(chart.total.sum).to eq(1)
+ end
end
context 'weekchart' do
@@ -47,6 +63,10 @@ RSpec.describe Gitlab::Ci::Charts do
subject { chart.to }
+ before do
+ create(:ci_empty_pipeline, project: project, duration: 120)
+ end
+
it 'includes the whole current day' do
is_expected.to eq(Date.today.end_of_day)
end
@@ -58,6 +78,68 @@ RSpec.describe Gitlab::Ci::Charts do
it 'uses %d %B as labels format' do
expect(chart.labels).to include(chart.from.strftime('%d %B'))
end
+
+ it 'returns count of pipelines run each day in the current week' do
+ expect(chart.total.sum).to eq(1)
+ end
+ end
+
+ context 'weekchart_utc' do
+ today = Date.today
+ end_of_today = Time.use_zone(Time.find_zone('UTC')) { today.end_of_day }
+
+ let(:project) { create(:project) }
+ let(:chart) do
+ allow(Date).to receive(:today).and_return(today)
+ allow(today).to receive(:end_of_day).and_return(end_of_today)
+ Gitlab::Ci::Charts::WeekChart.new(project)
+ end
+
+ subject { chart.total }
+
+ before do
+ create(:ci_empty_pipeline, project: project, duration: 120)
+ end
+
+ it 'uses a utc time zone for range times' do
+ expect(chart.to.zone).to eq(end_of_today.zone)
+ expect(chart.from.zone).to eq(end_of_today.zone)
+ end
+
+ it 'returns count of pipelines run each day in the current week' do
+ expect(chart.total.sum).to eq(1)
+ end
+ end
+
+ context 'weekchart_non_utc' do
+ today = Date.today
+ end_of_today = Time.use_zone(Time.find_zone('Asia/Dubai')) { today.end_of_day }
+
+ let(:project) { create(:project) }
+ let(:chart) do
+ allow(Date).to receive(:today).and_return(today)
+ allow(today).to receive(:end_of_day).and_return(end_of_today)
+ Gitlab::Ci::Charts::WeekChart.new(project)
+ end
+
+ subject { chart.total }
+
+ before do
+ # The DB uses UTC always, so our use of a Time Zone in the application
+ # can cause the creation date of the pipeline to go unmatched depending
+ # on the offset. We can work around this by requesting the pipeline be
+ # created a with the `created_at` field set to a day ago in the same week.
+ create(:ci_empty_pipeline, project: project, duration: 120, created_at: today - 1.day)
+ end
+
+ it 'uses a non-utc time zone for range times' do
+ expect(chart.to.zone).to eq(end_of_today.zone)
+ expect(chart.from.zone).to eq(end_of_today.zone)
+ end
+
+ it 'returns count of pipelines run each day in the current week' do
+ expect(chart.total.sum).to eq(1)
+ end
end
context 'pipeline_times' do
diff --git a/spec/lib/gitlab/ci/config/entry/cache_spec.rb b/spec/lib/gitlab/ci/config/entry/cache_spec.rb
index 80427eaa6ee..247f4b63910 100644
--- a/spec/lib/gitlab/ci/config/entry/cache_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/cache_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::Config::Entry::Cache do
+ using RSpec::Parameterized::TableSyntax
+
subject(:entry) { described_class.new(config) }
describe 'validations' do
@@ -56,8 +58,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do
end
context 'with `policy`' do
- using RSpec::Parameterized::TableSyntax
-
where(:policy, :result) do
'pull-push' | 'pull-push'
'push' | 'push'
@@ -77,8 +77,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do
end
context 'with `when`' do
- using RSpec::Parameterized::TableSyntax
-
where(:when_config, :result) do
'on_success' | 'on_success'
'on_failure' | 'on_failure'
@@ -109,8 +107,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do
end
context 'with `policy`' do
- using RSpec::Parameterized::TableSyntax
-
where(:policy, :valid) do
'pull-push' | true
'push' | true
@@ -126,8 +122,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do
end
context 'with `when`' do
- using RSpec::Parameterized::TableSyntax
-
where(:when_config, :valid) do
'on_success' | true
'on_failure' | true
diff --git a/spec/lib/gitlab/ci/config/entry/commands_spec.rb b/spec/lib/gitlab/ci/config/entry/commands_spec.rb
index 439799fe973..1b8dfae692a 100644
--- a/spec/lib/gitlab/ci/config/entry/commands_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/commands_spec.rb
@@ -87,18 +87,20 @@ RSpec.describe Gitlab::Ci::Config::Entry::Commands do
describe '#errors' do
it 'saves errors' do
expect(entry.errors)
- .to include 'commands config should be a string or an array containing strings and arrays of strings'
+ .to include 'commands config should be a string or a nested array of strings up to 10 levels deep'
end
end
end
context 'when entry value is multi-level nested array' do
- let(:config) { [['ls', ['echo 1']], 'pwd'] }
+ let(:config) do
+ ['ls 0', ['ls 1', ['ls 2', ['ls 3', ['ls 4', ['ls 5', ['ls 6', ['ls 7', ['ls 8', ['ls 9', ['ls 10']]]]]]]]]]]
+ end
describe '#errors' do
it 'saves errors' do
expect(entry.errors)
- .to include 'commands config should be a string or an array containing strings and arrays of strings'
+ .to include 'commands config should be a string or a nested array of strings up to 10 levels deep'
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 7834a1a94f2..a3b5f32b9f9 100644
--- a/spec/lib/gitlab/ci/config/entry/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb
@@ -763,16 +763,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do
it 'returns allow_failure_criteria' do
expect(entry.value[:allow_failure_criteria]).to match(exit_codes: [42])
end
-
- context 'with ci_allow_failure_with_exit_codes disabled' do
- before do
- stub_feature_flags(ci_allow_failure_with_exit_codes: false)
- end
-
- it 'does not return allow_failure_criteria' do
- expect(entry.value.key?(:allow_failure_criteria)).to be_falsey
- end
- end
end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/processable_spec.rb b/spec/lib/gitlab/ci/config/entry/processable_spec.rb
index aadf94365c6..04e80450263 100644
--- a/spec/lib/gitlab/ci/config/entry/processable_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/processable_spec.rb
@@ -73,6 +73,15 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable do
end
end
+ context 'when resource_group key is not a string' do
+ let(:config) { { resource_group: 123 } }
+
+ it 'returns error about wrong value type' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include "job resource group should be a string"
+ end
+ end
+
context 'when it uses both "when:" and "rules:"' do
let(:config) do
{
@@ -340,6 +349,26 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable do
end
end
+ context 'with resource group' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:resource_group, :result) do
+ 'iOS' | 'iOS'
+ 'review/$CI_COMMIT_REF_NAME' | 'review/$CI_COMMIT_REF_NAME'
+ nil | nil
+ end
+
+ with_them do
+ let(:config) { { script: 'ls', resource_group: resource_group }.compact }
+
+ it do
+ entry.compose!(deps)
+
+ expect(entry.resource_group).to eq(result)
+ end
+ end
+ end
+
context 'with inheritance' do
context 'of variables' do
let(:config) do
diff --git a/spec/lib/gitlab/ci/config/external/mapper_spec.rb b/spec/lib/gitlab/ci/config/external/mapper_spec.rb
index 4fdaaca8316..99f546ceb37 100644
--- a/spec/lib/gitlab/ci/config/external/mapper_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/mapper_spec.rb
@@ -323,20 +323,6 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper do
expect { subject }.to raise_error(described_class::AmbigiousSpecificationError)
end
end
-
- context 'when feature flag is turned off' do
- let(:values) do
- { include: full_local_file_path }
- end
-
- before do
- stub_feature_flags(variables_in_include_section_ci: false)
- end
-
- it 'does not expand the variables' do
- expect(subject[0].location).to eq('$CI_PROJECT_PATH' + local_file)
- end
- end
end
end
end
diff --git a/spec/lib/gitlab/ci/config/yaml/tags/reference_spec.rb b/spec/lib/gitlab/ci/config/yaml/tags/reference_spec.rb
new file mode 100644
index 00000000000..c68dccd3455
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/yaml/tags/reference_spec.rb
@@ -0,0 +1,121 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Config::Yaml::Tags::Reference do
+ let(:config) do
+ Gitlab::Ci::Config::Yaml.load!(yaml)
+ end
+
+ describe '.tag' do
+ it 'implements the tag method' do
+ expect(described_class.tag).to eq('!reference')
+ end
+ end
+
+ describe '#resolve' do
+ subject { Gitlab::Ci::Config::Yaml::Tags::Resolver.new(config).to_hash }
+
+ context 'with circular references' do
+ let(:yaml) do
+ <<~YML
+ a: !reference [b]
+ b: !reference [a]
+ YML
+ end
+
+ it 'raises CircularReferenceError' do
+ expect { subject }.to raise_error Gitlab::Ci::Config::Yaml::Tags::TagError, '!reference ["b"] is part of a circular chain'
+ end
+ end
+
+ context 'with nested circular references' do
+ let(:yaml) do
+ <<~YML
+ a: !reference [b, c]
+ b: { c: !reference [d, e, f] }
+ d: { e: { f: !reference [a] } }
+ YML
+ end
+
+ it 'raises CircularReferenceError' do
+ expect { subject }.to raise_error Gitlab::Ci::Config::Yaml::Tags::TagError, '!reference ["b", "c"] is part of a circular chain'
+ end
+ end
+
+ context 'with missing references' do
+ let(:yaml) { 'a: !reference [b]' }
+
+ it 'raises MissingReferenceError' do
+ expect { subject }.to raise_error Gitlab::Ci::Config::Yaml::Tags::TagError, '!reference ["b"] could not be found'
+ end
+ end
+
+ context 'with invalid references' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:yaml, :error_message) do
+ 'a: !reference' | '!reference [] is not valid'
+ 'a: !reference str' | '!reference "str" is not valid'
+ 'a: !reference 1' | '!reference "1" is not valid'
+ 'a: !reference [1]' | '!reference [1] is not valid'
+ 'a: !reference { b: c }' | '!reference {"b"=>"c"} is not valid'
+ end
+
+ with_them do
+ it 'raises an error' do
+ expect { subject }.to raise_error Gitlab::Ci::Config::Yaml::Tags::TagError, error_message
+ end
+ end
+ end
+
+ context 'with arrays' do
+ let(:yaml) do
+ <<~YML
+ a: { b: [1, 2] }
+ c: { d: { e: [3, 4] } }
+ f: { g: [ !reference [a, b], 5, !reference [c, d, e]] }
+ YML
+ end
+
+ it { is_expected.to match(a_hash_including({ f: { g: [[1, 2], 5, [3, 4]] } })) }
+ end
+
+ context 'with hashes' do
+ context 'when referencing an entire hash' do
+ let(:yaml) do
+ <<~YML
+ a: { b: { c: 'c', d: 'd' } }
+ e: { f: !reference [a, b] }
+ YML
+ end
+
+ it { is_expected.to match(a_hash_including({ e: { f: { c: 'c', d: 'd' } } })) }
+ end
+
+ context 'when referencing only a hash value' do
+ let(:yaml) do
+ <<~YML
+ a: { b: { c: 'c', d: 'd' } }
+ e: { f: { g: !reference [a, b, c], h: 'h' } }
+ i: !reference [e, f]
+ YML
+ end
+
+ it { is_expected.to match(a_hash_including({ i: { g: 'c', h: 'h' } })) }
+ end
+
+ context 'when referencing a value before its definition' do
+ let(:yaml) do
+ <<~YML
+ a: { b: !reference [c, d] }
+ g: { h: { i: 'i', j: 1 } }
+ c: { d: { e: !reference [g, h, j], f: 'f' } }
+ YML
+ end
+
+ it { is_expected.to match(a_hash_including({ a: { b: { e: 1, f: 'f' } } })) }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/yaml/tags/resolver_spec.rb b/spec/lib/gitlab/ci/config/yaml/tags/resolver_spec.rb
new file mode 100644
index 00000000000..594242c33cc
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/yaml/tags/resolver_spec.rb
@@ -0,0 +1,123 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Config::Yaml::Tags::Resolver do
+ let(:config) do
+ Gitlab::Ci::Config::Yaml.load!(yaml)
+ end
+
+ describe '#to_hash' do
+ subject { described_class.new(config).to_hash }
+
+ context 'when referencing deeply nested arrays' do
+ let(:yaml_templates) do
+ <<~YML
+ .job-1:
+ script:
+ - echo doing step 1 of job 1
+ - echo doing step 2 of job 1
+
+ .job-2:
+ script:
+ - echo doing step 1 of job 2
+ - !reference [.job-1, script]
+ - echo doing step 2 of job 2
+
+ .job-3:
+ script:
+ - echo doing step 1 of job 3
+ - !reference [.job-2, script]
+ - echo doing step 2 of job 3
+ YML
+ end
+
+ let(:job_yaml) do
+ <<~YML
+ test:
+ script:
+ - echo preparing to test
+ - !reference [.job-3, script]
+ - echo test finished
+ YML
+ end
+
+ shared_examples 'expands references' do
+ it 'expands the references' do
+ is_expected.to match({
+ '.job-1': {
+ script: [
+ 'echo doing step 1 of job 1',
+ 'echo doing step 2 of job 1'
+ ]
+ },
+ '.job-2': {
+ script: [
+ 'echo doing step 1 of job 2',
+ [
+ 'echo doing step 1 of job 1',
+ 'echo doing step 2 of job 1'
+ ],
+ 'echo doing step 2 of job 2'
+ ]
+ },
+ '.job-3': {
+ script: [
+ 'echo doing step 1 of job 3',
+ [
+ 'echo doing step 1 of job 2',
+ [
+ 'echo doing step 1 of job 1',
+ 'echo doing step 2 of job 1'
+ ],
+ 'echo doing step 2 of job 2'
+ ],
+ 'echo doing step 2 of job 3'
+ ]
+ },
+ test: {
+ script: [
+ 'echo preparing to test',
+ [
+ 'echo doing step 1 of job 3',
+ [
+ 'echo doing step 1 of job 2',
+ [
+ 'echo doing step 1 of job 1',
+ 'echo doing step 2 of job 1'
+ ],
+ 'echo doing step 2 of job 2'
+ ],
+ 'echo doing step 2 of job 3'
+ ],
+ 'echo test finished'
+ ]
+ }
+ })
+ end
+ end
+
+ context 'when templates are defined before the job' do
+ let(:yaml) do
+ <<~YML
+ #{yaml_templates}
+ #{job_yaml}
+ YML
+ end
+
+ it_behaves_like 'expands references'
+ end
+
+ context 'when templates are defined after the job' do
+ let(:yaml) do
+ <<~YML
+ #{job_yaml}
+ #{yaml_templates}
+ YML
+ end
+
+ it_behaves_like 'expands references'
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb
index dc03d2f80fe..45ce4cac6c4 100644
--- a/spec/lib/gitlab/ci/config_spec.rb
+++ b/spec/lib/gitlab/ci/config_spec.rb
@@ -263,6 +263,26 @@ RSpec.describe Gitlab::Ci::Config do
end
end
end
+
+ context 'when yaml uses circular !reference' do
+ let(:yml) do
+ <<~YAML
+ job-1:
+ script:
+ - !reference [job-2, before_script]
+
+ job-2:
+ before_script: !reference [job-1, script]
+ YAML
+ end
+
+ it 'raises error' do
+ expect { config }.to raise_error(
+ described_class::ConfigError,
+ /\!reference \["job-2", "before_script"\] is part of a circular chain/
+ )
+ end
+ end
end
context "when using 'include' directive" do
diff --git a/spec/lib/gitlab/ci/cron_parser_spec.rb b/spec/lib/gitlab/ci/cron_parser_spec.rb
index dd27b4045c9..15293429354 100644
--- a/spec/lib/gitlab/ci/cron_parser_spec.rb
+++ b/spec/lib/gitlab/ci/cron_parser_spec.rb
@@ -63,6 +63,17 @@ RSpec.describe Gitlab::Ci::CronParser do
end
end
+ context 'when range and slash used' do
+ let(:cron) { '3-59/10 * * * *' }
+ let(:cron_timezone) { 'UTC' }
+
+ it_behaves_like returns_time_for_epoch
+
+ it 'returns specific time' do
+ expect(subject.min).to be_in([3, 13, 23, 33, 43, 53])
+ end
+ end
+
context 'when cron_timezone is TZInfo format' do
before do
allow(Time).to receive(:zone)
diff --git a/spec/lib/gitlab/ci/jwt_spec.rb b/spec/lib/gitlab/ci/jwt_spec.rb
index 3130c0c0c41..342ca6b8b75 100644
--- a/spec/lib/gitlab/ci/jwt_spec.rb
+++ b/spec/lib/gitlab/ci/jwt_spec.rb
@@ -44,6 +44,9 @@ RSpec.describe Gitlab::Ci::Jwt do
expect(payload[:pipeline_id]).to eq(pipeline.id.to_s)
expect(payload[:job_id]).to eq(build.id.to_s)
expect(payload[:ref]).to eq(pipeline.source_ref)
+ expect(payload[:ref_protected]).to eq(build.protected.to_s)
+ expect(payload[:environment]).to be_nil
+ expect(payload[:environment_protected]).to be_nil
end
end
@@ -90,6 +93,39 @@ RSpec.describe Gitlab::Ci::Jwt do
expect(payload[:ref_protected]).to eq('true')
end
end
+
+ describe 'environment' do
+ let(:environment) { build_stubbed(:environment, project: project, name: 'production') }
+ let(:build) do
+ build_stubbed(
+ :ci_build,
+ project: project,
+ user: user,
+ pipeline: pipeline,
+ environment: environment.name
+ )
+ end
+
+ before do
+ allow(build).to receive(:persisted_environment).and_return(environment)
+ end
+
+ it 'has correct values for environment attributes' do
+ expect(payload[:environment]).to eq('production')
+ expect(payload[:environment_protected]).to eq('false')
+ end
+
+ context ':ci_jwt_include_environment feature flag is disabled' do
+ before do
+ stub_feature_flags(ci_jwt_include_environment: false)
+ end
+
+ it 'does not include environment attributes' do
+ expect(payload).not_to have_key(:environment)
+ expect(payload).not_to have_key(:environment_protected)
+ end
+ end
+ end
end
describe '.for_build' do
diff --git a/spec/lib/gitlab/ci/parsers/instrumentation_spec.rb b/spec/lib/gitlab/ci/parsers/instrumentation_spec.rb
new file mode 100644
index 00000000000..30bcce21be2
--- /dev/null
+++ b/spec/lib/gitlab/ci/parsers/instrumentation_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Parsers::Instrumentation do
+ describe '#parse!' do
+ let(:parser_class) do
+ Class.new do
+ prepend Gitlab::Ci::Parsers::Instrumentation
+
+ def parse!(arg1, arg2)
+ "parse #{arg1} #{arg2}"
+ end
+ end
+ end
+
+ it 'sets metrics for duration of parsing' do
+ result = parser_class.new.parse!('hello', 'world')
+
+ expect(result).to eq('parse hello world')
+
+ metrics = Gitlab::Metrics.registry.get(:ci_report_parser_duration_seconds).get({ parser: parser_class.name })
+
+ expect(metrics.keys).to match_array(described_class::BUCKETS)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/parsers_spec.rb b/spec/lib/gitlab/ci/parsers_spec.rb
index b932cd81272..c9891c06507 100644
--- a/spec/lib/gitlab/ci/parsers_spec.rb
+++ b/spec/lib/gitlab/ci/parsers_spec.rb
@@ -54,4 +54,12 @@ RSpec.describe Gitlab::Ci::Parsers do
end
end
end
+
+ describe '.instrument!' do
+ it 'prepends the Instrumentation module into each parser' do
+ expect(described_class.parsers.values).to all( receive(:prepend).with(Gitlab::Ci::Parsers::Instrumentation) )
+
+ described_class.instrument!
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
index 20406acb658..53dea1d0d19 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
@@ -235,7 +235,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Build do
with_them do
before do
- project.update!(ci_keep_latest_artifact: keep_latest_artifact)
+ project.update!(keep_latest_artifact: keep_latest_artifact)
end
it 'builds a pipeline with appropriate locked value' do
diff --git a/spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb
index 3eaecb11ae0..1d17244e519 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb
@@ -58,20 +58,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::CancelPendingPipelines do
expect(build_statuses(child_pipeline)).to contain_exactly('canceled')
end
-
- context 'when FF ci_auto_cancel_all_pipelines is disabled' do
- before do
- stub_feature_flags(ci_auto_cancel_all_pipelines: false)
- end
-
- it 'does not cancel interruptible builds of child pipeline' do
- expect(build_statuses(child_pipeline)).to contain_exactly('running')
-
- perform
-
- expect(build_statuses(child_pipeline)).to contain_exactly('running')
- end
- end
end
context 'when the child pipeline has not an interruptible job' do
diff --git a/spec/lib/gitlab/ci/pipeline/chain/template_usage_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/template_usage_spec.rb
index 3616461d94f..cd868a57bbc 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/template_usage_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/template_usage_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::TemplateUsage do
%w(Template-1 Template-2).each do |expected_template|
expect(Gitlab::UsageDataCounters::CiTemplateUniqueCounter).to(
receive(:track_unique_project_event)
- .with(project_id: project.id, template: expected_template)
+ .with(project_id: project.id, template: expected_template, config_source: pipeline.config_source)
)
end
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
index cf020fc343c..0efc7484699 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
@@ -383,14 +383,25 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
end
context 'when job is a bridge' do
- let(:attributes) do
+ let(:base_attributes) do
{
name: 'rspec', ref: 'master', options: { trigger: 'my/project' }, scheduling_type: :stage
}
end
+ let(:attributes) { base_attributes }
+
it { is_expected.to be_a(::Ci::Bridge) }
it { is_expected.to be_valid }
+
+ context 'when job belongs to a resource group' do
+ let(:attributes) { base_attributes.merge(resource_group_key: 'iOS') }
+
+ it 'returns a job with resource group' do
+ expect(subject.resource_group).not_to be_nil
+ expect(subject.resource_group.key).to eq('iOS')
+ end
+ end
end
it 'memoizes a resource object' do
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build/resource_group_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/processable/resource_group_spec.rb
index 8fcc242ba5f..b7260599de2 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/build/resource_group_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/processable/resource_group_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Pipeline::Seed::Build::ResourceGroup do
+RSpec.describe Gitlab::Ci::Pipeline::Seed::Processable::ResourceGroup do
let_it_be(:project) { create(:project) }
let(:job) { build(:ci_build, project: project) }
let(:seed) { described_class.new(job, resource_group_key) }
diff --git a/spec/lib/gitlab/ci/reports/codequality_mr_diff_spec.rb b/spec/lib/gitlab/ci/reports/codequality_mr_diff_spec.rb
new file mode 100644
index 00000000000..8b177fa7fc1
--- /dev/null
+++ b/spec/lib/gitlab/ci/reports/codequality_mr_diff_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Reports::CodequalityMrDiff do
+ let(:codequality_report) { Gitlab::Ci::Reports::CodequalityReports.new }
+ let(:degradation_1) { build(:codequality_degradation_1) }
+ let(:degradation_2) { build(:codequality_degradation_2) }
+ let(:degradation_3) { build(:codequality_degradation_3) }
+
+ describe '#initialize!' do
+ subject(:report) { described_class.new(codequality_report) }
+
+ context 'when quality has degradations' do
+ context 'with several degradations on the same line' do
+ before do
+ codequality_report.add_degradation(degradation_1)
+ codequality_report.add_degradation(degradation_2)
+ end
+
+ it 'generates quality report for mr diff' do
+ expect(report.files).to match(
+ "file_a.rb" => [
+ { line: 10, description: "Avoid parameter lists longer than 5 parameters. [12/5]", severity: "major" },
+ { line: 10, description: "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.", severity: "major" }
+ ]
+ )
+ end
+ end
+
+ context 'with several degradations on several files' do
+ before do
+ codequality_report.add_degradation(degradation_1)
+ codequality_report.add_degradation(degradation_2)
+ codequality_report.add_degradation(degradation_3)
+ end
+
+ it 'returns quality report for mr diff' do
+ expect(report.files).to match(
+ "file_a.rb" => [
+ { line: 10, description: "Avoid parameter lists longer than 5 parameters. [12/5]", severity: "major" },
+ { line: 10, description: "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.", severity: "major" }
+ ],
+ "file_b.rb" => [
+ { line: 10, description: "Avoid parameter lists longer than 5 parameters. [12/5]", severity: "minor" }
+ ]
+ )
+ end
+ end
+ end
+
+ context 'when quality has no degradation' do
+ it 'returns an empty hash' do
+ expect(report.files).to match({})
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/reports/codequality_reports_comparer_spec.rb b/spec/lib/gitlab/ci/reports/codequality_reports_comparer_spec.rb
index 7053d54381b..90188b56f5a 100644
--- a/spec/lib/gitlab/ci/reports/codequality_reports_comparer_spec.rb
+++ b/spec/lib/gitlab/ci/reports/codequality_reports_comparer_spec.rb
@@ -6,62 +6,8 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReportsComparer do
let(:comparer) { described_class.new(base_report, head_report) }
let(:base_report) { Gitlab::Ci::Reports::CodequalityReports.new }
let(:head_report) { Gitlab::Ci::Reports::CodequalityReports.new }
- let(:degradation_1) 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"
- }.with_indifferent_access
- end
-
- let(:degradation_2) do
- {
- "type": "Issue",
- "check_name": "Rubocop/Metrics/ParameterLists",
- "description": "Avoid parameter lists longer than 5 parameters. [12/5]",
- "categories": [
- "Complexity"
- ],
- "remediation_points": 550000,
- "location": {
- "path": "foo.rb",
- "positions": {
- "begin": {
- "column": 14,
- "line": 10
- },
- "end": {
- "column": 39,
- "line": 10
- }
- }
- },
- "content": {
- "body": "This cop checks for methods with too many parameters.\nThe maximum number of parameters is configurable.\nKeyword arguments can optionally be excluded from the total count."
- },
- "engine_name": "rubocop",
- "fingerprint": "ab5f8b935886b942d621399f5a2ca16e",
- "severity": "minor"
- }.with_indifferent_access
- end
+ let(:degradation_1) { build(:codequality_degradation_1) }
+ let(:degradation_2) { build(:codequality_degradation_2) }
describe '#status' do
subject(:report_status) { comparer.status }
diff --git a/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb b/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb
index 44e67259369..ae9b2f2c62b 100644
--- a/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb
+++ b/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb
@@ -4,62 +4,8 @@ require 'spec_helper'
RSpec.describe Gitlab::Ci::Reports::CodequalityReports do
let(:codequality_report) { described_class.new }
- let(:degradation_1) 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"
- }.with_indifferent_access
- end
-
- let(:degradation_2) do
- {
- "type": "Issue",
- "check_name": "Rubocop/Metrics/ParameterLists",
- "description": "Avoid parameter lists longer than 5 parameters. [12/5]",
- "categories": [
- "Complexity"
- ],
- "remediation_points": 550000,
- "location": {
- "path": "foo.rb",
- "positions": {
- "begin": {
- "column": 14,
- "line": 10
- },
- "end": {
- "column": 39,
- "line": 10
- }
- }
- },
- "content": {
- "body": "This cop checks for methods with too many parameters.\nThe maximum number of parameters is configurable.\nKeyword arguments can optionally be excluded from the total count."
- },
- "engine_name": "rubocop",
- "fingerprint": "ab5f8b935886b942d621399f5a2ca16e",
- "severity": "minor"
- }.with_indifferent_access
- end
+ let(:degradation_1) { build(:codequality_degradation_1) }
+ let(:degradation_2) { build(:codequality_degradation_2) }
it { expect(codequality_report.degradations).to eq({}) }
diff --git a/spec/lib/gitlab/ci/status/bridge/factory_spec.rb b/spec/lib/gitlab/ci/status/bridge/factory_spec.rb
index d27bb98ba9a..6081f104e42 100644
--- a/spec/lib/gitlab/ci/status/bridge/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/bridge/factory_spec.rb
@@ -117,14 +117,31 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Factory do
end
end
+ context 'when bridge is waiting for resource' do
+ let(:bridge) { create_bridge(:waiting_for_resource, :resource_group) }
+
+ it 'matches correct core status' do
+ expect(factory.core_status).to be_a Gitlab::Ci::Status::WaitingForResource
+ end
+
+ it 'fabricates status with correct details' do
+ expect(status.text).to eq 'waiting'
+ expect(status.group).to eq 'waiting-for-resource'
+ expect(status.icon).to eq 'status_pending'
+ expect(status.favicon).to eq 'favicon_pending'
+ expect(status.illustration).to include(:image, :size, :title)
+ expect(status).not_to have_details
+ end
+ end
+
private
- def create_bridge(trait)
+ def create_bridge(*traits)
upstream_project = create(:project, :repository)
downstream_project = create(:project, :repository)
upstream_pipeline = create(:ci_pipeline, :running, project: upstream_project)
trigger = { trigger: { project: downstream_project.full_path, branch: 'feature' } }
- create(:ci_bridge, trait, options: trigger, pipeline: upstream_pipeline)
+ create(:ci_bridge, *traits, options: trigger, pipeline: upstream_pipeline)
end
end
diff --git a/spec/lib/gitlab/ci/status/bridge/waiting_for_resource_spec.rb b/spec/lib/gitlab/ci/status/bridge/waiting_for_resource_spec.rb
new file mode 100644
index 00000000000..3e19df28d83
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/bridge/waiting_for_resource_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Ci::Status::Bridge::WaitingForResource do
+ it { expect(described_class).to be < Gitlab::Ci::Status::Processable::WaitingForResource }
+end
diff --git a/spec/lib/gitlab/ci/status/build/waiting_for_resource_spec.rb b/spec/lib/gitlab/ci/status/build/waiting_for_resource_spec.rb
new file mode 100644
index 00000000000..44bd5a8611a
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/build/waiting_for_resource_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Ci::Status::Build::WaitingForResource do
+ it { expect(described_class).to be < Gitlab::Ci::Status::Processable::WaitingForResource }
+end
diff --git a/spec/lib/gitlab/ci/status/processable/waiting_for_resource_spec.rb b/spec/lib/gitlab/ci/status/processable/waiting_for_resource_spec.rb
new file mode 100644
index 00000000000..91a9724d043
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/processable/waiting_for_resource_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Status::Processable::WaitingForResource do
+ let(:user) { create(:user) }
+
+ subject do
+ processable = create(:ci_build, :waiting_for_resource, :resource_group)
+ described_class.new(Gitlab::Ci::Status::Core.new(processable, user))
+ end
+
+ describe '#illustration' do
+ it { expect(subject.illustration).to include(:image, :size, :title) }
+ end
+
+ describe '.matches?' do
+ subject {described_class.matches?(processable, user) }
+
+ context 'when processable is waiting for resource' do
+ let(:processable) { create(:ci_build, :waiting_for_resource) }
+
+ it 'is a correct match' do
+ expect(subject).to be true
+ end
+ end
+
+ context 'when processable is not waiting for resource' do
+ let(:processable) { create(:ci_build) }
+
+ it 'does not match' do
+ expect(subject).to be false
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/trace/chunked_io_spec.rb b/spec/lib/gitlab/ci/trace/chunked_io_spec.rb
index a2903391c6f..f09e03b4d55 100644
--- a/spec/lib/gitlab/ci/trace/chunked_io_spec.rb
+++ b/spec/lib/gitlab/ci/trace/chunked_io_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Gitlab::Ci::Trace::ChunkedIO, :clean_gitlab_redis_cache do
let(:chunked_io) { described_class.new(build) }
before do
- stub_feature_flags(ci_enable_live_trace: true)
+ stub_feature_flags(ci_enable_live_trace: true, gitlab_ci_trace_read_consistency: true)
end
describe "#initialize" do
diff --git a/spec/lib/gitlab/ci/variables/collection/sorted_spec.rb b/spec/lib/gitlab/ci/variables/collection/sorted_spec.rb
index d85bf29f77f..954273fd41e 100644
--- a/spec/lib/gitlab/ci/variables/collection/sorted_spec.rb
+++ b/spec/lib/gitlab/ci/variables/collection/sorted_spec.rb
@@ -5,8 +5,11 @@ require 'spec_helper'
RSpec.describe Gitlab::Ci::Variables::Collection::Sorted do
describe '#errors' do
context 'when FF :variable_inside_variable is disabled' do
+ let_it_be(:project_with_flag_disabled) { create(:project) }
+ let_it_be(:project_with_flag_enabled) { create(:project) }
+
before do
- stub_feature_flags(variable_inside_variable: false)
+ stub_feature_flags(variable_inside_variable: [project_with_flag_enabled])
end
context 'table tests' do
@@ -53,7 +56,7 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Sorted do
end
with_them do
- subject { Gitlab::Ci::Variables::Collection::Sorted.new(variables) }
+ subject { Gitlab::Ci::Variables::Collection::Sorted.new(variables, project_with_flag_disabled) }
it 'does not report error' do
expect(subject.errors).to eq(nil)
@@ -67,8 +70,11 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Sorted do
end
context 'when FF :variable_inside_variable is enabled' do
+ let_it_be(:project_with_flag_disabled) { create(:project) }
+ let_it_be(:project_with_flag_enabled) { create(:project) }
+
before do
- stub_feature_flags(variable_inside_variable: true)
+ stub_feature_flags(variable_inside_variable: [project_with_flag_enabled])
end
context 'table tests' do
@@ -100,7 +106,7 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Sorted do
end
with_them do
- subject { Gitlab::Ci::Variables::Collection::Sorted.new(variables) }
+ subject { Gitlab::Ci::Variables::Collection::Sorted.new(variables, project_with_flag_enabled) }
it 'errors matches expected validation result' do
expect(subject.errors).to eq(validation_result)
@@ -164,7 +170,8 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Sorted do
end
with_them do
- subject { Gitlab::Ci::Variables::Collection::Sorted.new(variables) }
+ let_it_be(:project) { create(:project) }
+ subject { Gitlab::Ci::Variables::Collection::Sorted.new(variables, project) }
it 'does not expand variables' do
expect(subject.sort).to eq(variables)
@@ -239,7 +246,8 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Sorted do
end
with_them do
- subject { Gitlab::Ci::Variables::Collection::Sorted.new(variables) }
+ let_it_be(:project) { create(:project) }
+ subject { Gitlab::Ci::Variables::Collection::Sorted.new(variables, project) }
it 'sort returns correctly sorted variables' do
expect(subject.sort.map { |var| var[:key] }).to eq(result)
diff --git a/spec/lib/gitlab/ci/variables/helpers_spec.rb b/spec/lib/gitlab/ci/variables/helpers_spec.rb
new file mode 100644
index 00000000000..b45abf8c0e1
--- /dev/null
+++ b/spec/lib/gitlab/ci/variables/helpers_spec.rb
@@ -0,0 +1,103 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Ci::Variables::Helpers do
+ describe '.merge_variables' do
+ let(:current_variables) do
+ [{ key: 'key1', value: 'value1' },
+ { key: 'key2', value: 'value2' }]
+ end
+
+ let(:new_variables) do
+ [{ key: 'key2', value: 'value22' },
+ { key: 'key3', value: 'value3' }]
+ end
+
+ let(:result) do
+ [{ key: 'key1', value: 'value1', public: true },
+ { key: 'key2', value: 'value22', public: true },
+ { key: 'key3', value: 'value3', public: true }]
+ end
+
+ subject { described_class.merge_variables(current_variables, new_variables) }
+
+ it { is_expected.to eq(result) }
+
+ context 'when new variables is a hash' do
+ let(:new_variables) do
+ { 'key2' => 'value22', 'key3' => 'value3' }
+ end
+
+ it { is_expected.to eq(result) }
+ end
+
+ context 'when new variables is a hash with symbol keys' do
+ let(:new_variables) do
+ { key2: 'value22', key3: 'value3' }
+ end
+
+ it { is_expected.to eq(result) }
+ end
+
+ context 'when new variables is nil' do
+ let(:new_variables) {}
+ let(:result) do
+ [{ key: 'key1', value: 'value1', public: true },
+ { key: 'key2', value: 'value2', public: true }]
+ end
+
+ it { is_expected.to eq(result) }
+ end
+ end
+
+ describe '.transform_to_yaml_variables' do
+ let(:variables) do
+ { 'key1' => 'value1', 'key2' => 'value2' }
+ end
+
+ let(:result) do
+ [{ key: 'key1', value: 'value1', public: true },
+ { key: 'key2', value: 'value2', public: true }]
+ end
+
+ subject { described_class.transform_to_yaml_variables(variables) }
+
+ it { is_expected.to eq(result) }
+
+ context 'when variables is nil' do
+ let(:variables) {}
+
+ it { is_expected.to eq([]) }
+ end
+ end
+
+ describe '.transform_from_yaml_variables' do
+ let(:variables) do
+ [{ key: 'key1', value: 'value1', public: true },
+ { key: 'key2', value: 'value2', public: true }]
+ end
+
+ let(:result) do
+ { 'key1' => 'value1', 'key2' => 'value2' }
+ end
+
+ subject { described_class.transform_from_yaml_variables(variables) }
+
+ it { is_expected.to eq(result) }
+
+ context 'when variables is nil' do
+ let(:variables) {}
+
+ it { is_expected.to eq({}) }
+ end
+
+ context 'when variables is a hash' do
+ let(:variables) do
+ { key1: 'value1', 'key2' => 'value2' }
+ end
+
+ it { is_expected.to eq(result) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/cleanup/orphan_job_artifact_files_spec.rb b/spec/lib/gitlab/cleanup/orphan_job_artifact_files_spec.rb
index 8a7425a4156..b5adb603dab 100644
--- a/spec/lib/gitlab/cleanup/orphan_job_artifact_files_spec.rb
+++ b/spec/lib/gitlab/cleanup/orphan_job_artifact_files_spec.rb
@@ -42,7 +42,8 @@ RSpec.describe Gitlab::Cleanup::OrphanJobArtifactFiles do
end
it 'stops when limit is reached' do
- cleanup = described_class.new(limit: 1)
+ stub_env('LIMIT', 1)
+ cleanup = described_class.new
mock_artifacts_found(cleanup, 'tmp/foo/bar/1', 'tmp/foo/bar/2')
diff --git a/spec/lib/gitlab/cluster/lifecycle_events_spec.rb b/spec/lib/gitlab/cluster/lifecycle_events_spec.rb
new file mode 100644
index 00000000000..4ed68d54680
--- /dev/null
+++ b/spec/lib/gitlab/cluster/lifecycle_events_spec.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'rspec-parameterized'
+
+RSpec.describe Gitlab::Cluster::LifecycleEvents do
+ # 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
+
+ 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]
+ end
+
+ before do
+ # disable blackout period to speed-up tests
+ stub_config(shutdown: { blackout_seconds: 0 })
+ end
+
+ with_them do
+ subject { replica.public_send(method) }
+
+ it 'executes all hooks' do
+ hook_names.each do |hook_name|
+ hook = double
+ replica.instance_variable_set(:"@#{hook_name}", [hook])
+
+ # ensure that proper hooks are called
+ expect(hook).to receive(:call)
+ expect(replica).to receive(:call).with(hook_name, anything).and_call_original
+ end
+
+ subject
+ end
+ end
+ end
+
+ describe '#call' do
+ let(:name) { :my_hooks }
+
+ subject { replica.send(:call, name, hooks) }
+
+ context 'when many hooks raise exception' do
+ let(:hooks) do
+ [
+ -> { raise 'Exception A' },
+ -> { raise 'Exception B' }
+ ]
+ end
+
+ context 'USE_FATAL_LIFECYCLE_EVENTS is set to default' do
+ it 'only first hook is executed and is fatal' do
+ expect(hooks[0]).to receive(:call).and_call_original
+ expect(hooks[1]).not_to receive(:call)
+
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).and_call_original
+ expect(replica).to receive(:warn).with('ERROR: The hook my_hooks failed with exception (RuntimeError) "Exception A".')
+
+ expect { subject }.to raise_error(described_class::FatalError, 'Exception A')
+ end
+ end
+
+ context 'when USE_FATAL_LIFECYCLE_EVENTS is disabled' do
+ before do
+ stub_const('Gitlab::Cluster::LifecycleEvents::USE_FATAL_LIFECYCLE_EVENTS', false)
+ end
+
+ it 'many hooks are executed and all exceptions are logged' do
+ expect(hooks[0]).to receive(:call).and_call_original
+ expect(hooks[1]).to receive(:call).and_call_original
+
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).twice.and_call_original
+ expect(replica).to receive(:warn).twice.and_call_original
+
+ expect { subject }.not_to raise_error
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/composer/cache_spec.rb b/spec/lib/gitlab/composer/cache_spec.rb
new file mode 100644
index 00000000000..00318ac14f9
--- /dev/null
+++ b/spec/lib/gitlab/composer/cache_spec.rb
@@ -0,0 +1,133 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Composer::Cache do
+ let_it_be(:package_name) { 'sample-project' }
+ let_it_be(:json) { { 'name' => package_name } }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :custom_repo, files: { 'composer.json' => json.to_json }, group: group) }
+ let(:branch) { project.repository.find_branch('master') }
+ let(:sha_regex) { /^[A-Fa-f0-9]{64}$/ }
+
+ shared_examples 'Composer create cache page' do
+ let(:expected_json) { ::Gitlab::Composer::VersionIndex.new(packages).to_json }
+
+ before do
+ stub_composer_cache_object_storage
+ end
+
+ it 'creates the cached page' do
+ expect { subject }.to change { Packages::Composer::CacheFile.count }.by(1)
+ cache_file = Packages::Composer::CacheFile.last
+ expect(cache_file.file_sha256).to eq package.reload.composer_metadatum.version_cache_sha
+ expect(cache_file.file.read).to eq expected_json
+ end
+ end
+
+ shared_examples 'Composer marks cache page for deletion' do
+ it 'marks the page for deletion' do
+ cache_file = Packages::Composer::CacheFile.last
+
+ freeze_time do
+ expect { subject }.to change { cache_file.reload.delete_at}.from(nil).to(1.day.from_now)
+ end
+ end
+ end
+
+ describe '#execute' do
+ subject { described_class.new(project: project, name: package_name).execute }
+
+ context 'creating packages' do
+ context 'with a pre-existing package' do
+ let(:package) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '1.0.0', json: json) }
+ let(:package2) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '2.0.0', json: json) }
+ let(:packages) { [package, package2] }
+
+ before do
+ package
+ described_class.new(project: project, name: package_name).execute
+ package.reload
+ package2
+ end
+
+ it 'updates the sha and creates the cache page' do
+ expect { subject }.to change { package2.reload.composer_metadatum.version_cache_sha }.from(nil).to(sha_regex)
+ .and change { package.reload.composer_metadatum.version_cache_sha }.to(sha_regex)
+ end
+
+ it_behaves_like 'Composer create cache page'
+ it_behaves_like 'Composer marks cache page for deletion'
+ end
+
+ context 'first package' do
+ let!(:package) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '1.0.0', json: json) }
+ let(:packages) { [package] }
+
+ it 'updates the sha and creates the cache page' do
+ expect { subject }.to change { package.reload.composer_metadatum.version_cache_sha }.from(nil).to(sha_regex)
+ end
+
+ it_behaves_like 'Composer create cache page'
+ end
+ end
+
+ context 'updating packages' do
+ let(:package) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '1.0.0', json: json) }
+ let(:package2) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '2.0.0', json: json) }
+ let(:packages) { [package, package2] }
+
+ before do
+ packages
+
+ described_class.new(project: project, name: package_name).execute
+
+ package.update!(version: '1.2.0')
+ package.reload
+ end
+
+ it_behaves_like 'Composer create cache page'
+ it_behaves_like 'Composer marks cache page for deletion'
+ end
+
+ context 'deleting packages' do
+ context 'when it is not the last package' do
+ let(:package) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '1.0.0', json: json) }
+ let(:package2) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '2.0.0', json: json) }
+ let(:packages) { [package] }
+
+ before do
+ package
+ package2
+
+ described_class.new(project: project, name: package_name).execute
+
+ package2.destroy!
+ end
+
+ it_behaves_like 'Composer create cache page'
+ it_behaves_like 'Composer marks cache page for deletion'
+ end
+
+ context 'when it is the last package' do
+ let!(:package) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '1.0.0', json: json) }
+ let!(:last_sha) do
+ described_class.new(project: project, name: package_name).execute
+ package.reload.composer_metadatum.version_cache_sha
+ end
+
+ before do
+ package.destroy!
+ end
+
+ subject { described_class.new(project: project, name: package_name, last_page_sha: last_sha).execute }
+
+ it_behaves_like 'Composer marks cache page for deletion'
+
+ it 'does not create a new page' do
+ expect { subject }.not_to change { Packages::Composer::CacheFile.count }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/composer/version_index_spec.rb b/spec/lib/gitlab/composer/version_index_spec.rb
index 4c4742d9f59..7b0ed703f42 100644
--- a/spec/lib/gitlab/composer/version_index_spec.rb
+++ b/spec/lib/gitlab/composer/version_index_spec.rb
@@ -15,7 +15,9 @@ RSpec.describe Gitlab::Composer::VersionIndex do
let(:packages) { [package1, package2] }
describe '#as_json' do
- subject(:index) { described_class.new(packages).as_json }
+ subject(:package_index) { index['packages'][package_name] }
+
+ let(:index) { described_class.new(packages).as_json }
def expected_json(package)
{
@@ -32,10 +34,16 @@ RSpec.describe Gitlab::Composer::VersionIndex do
end
it 'returns the packages json' do
- packages = index['packages'][package_name]
+ expect(package_index['1.0.0']).to eq(expected_json(package1))
+ expect(package_index['2.0.0']).to eq(expected_json(package2))
+ end
+
+ context 'with an unordered list of packages' do
+ let(:packages) { [package2, package1] }
- expect(packages['1.0.0']).to eq(expected_json(package1))
- expect(packages['2.0.0']).to eq(expected_json(package2))
+ it 'returns the packages sorted by version' do
+ expect(package_index.keys).to eq ['1.0.0', '2.0.0']
+ end
end
end
diff --git a/spec/lib/gitlab/conan_token_spec.rb b/spec/lib/gitlab/conan_token_spec.rb
index be1d3e757f5..00683cf6e47 100644
--- a/spec/lib/gitlab/conan_token_spec.rb
+++ b/spec/lib/gitlab/conan_token_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Gitlab::ConanToken do
let(:jwt_secret) do
OpenSSL::HMAC.hexdigest(
- OpenSSL::Digest::SHA256.new,
+ OpenSSL::Digest.new('SHA256'),
base_secret,
described_class::HMAC_KEY
)
diff --git a/spec/lib/gitlab/config/entry/validators/nested_array_helpers_spec.rb b/spec/lib/gitlab/config/entry/validators/nested_array_helpers_spec.rb
new file mode 100644
index 00000000000..cd68307e71f
--- /dev/null
+++ b/spec/lib/gitlab/config/entry/validators/nested_array_helpers_spec.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Config::Entry::Validators::NestedArrayHelpers do
+ let(:config_struct) do
+ Struct.new(:value, keyword_init: true) do
+ include ActiveModel::Validations
+ extend Gitlab::Config::Entry::Validators::NestedArrayHelpers
+
+ validates_each :value do |record, attr, value|
+ unless validate_nested_array(value, 2) { |v| v.is_a?(Integer) }
+ record.errors.add(attr, "is invalid")
+ end
+ end
+ end
+ end
+
+ describe '#validate_nested_array' do
+ let(:config) { config_struct.new(value: value) }
+
+ subject(:errors) { config.errors }
+
+ before do
+ config.valid?
+ end
+
+ context 'with valid values' do
+ context 'with arrays of integers' do
+ let(:value) { [10, 11] }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'with nested arrays of integers' do
+ let(:value) { [10, [11, 12]] }
+
+ it { is_expected.to be_empty }
+ end
+ end
+
+ context 'with invalid values' do
+ subject(:error_messages) { errors.messages }
+
+ context 'with single integers' do
+ let(:value) { 10 }
+
+ it { is_expected.to eq({ value: ['is invalid'] }) }
+ end
+
+ context 'when it is nested over the limit' do
+ let(:value) { [10, [11, [12]]] }
+
+ it { is_expected.to eq({ value: ['is invalid'] }) }
+ end
+
+ context 'when a value in the array is not valid' do
+ let(:value) { [10, 11.5] }
+
+ it { is_expected.to eq({ value: ['is invalid'] }) }
+ end
+
+ context 'when a value in the nested array is not valid' do
+ let(:value) { [10, [11, 12.5]] }
+
+ it { is_expected.to eq({ value: ['is invalid'] }) }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/crypto_helper_spec.rb b/spec/lib/gitlab/crypto_helper_spec.rb
index c07089d8ef0..024564ea213 100644
--- a/spec/lib/gitlab/crypto_helper_spec.rb
+++ b/spec/lib/gitlab/crypto_helper_spec.rb
@@ -19,21 +19,85 @@ RSpec.describe Gitlab::CryptoHelper do
expect(encrypted).to match %r{\A[A-Za-z0-9+/=]+\z}
expect(encrypted).not_to include "\n"
end
+
+ it 'does not save hashed token with iv value in database' do
+ expect { described_class.aes256_gcm_encrypt('some-value') }.not_to change { TokenWithIv.count }
+ end
+
+ it 'encrypts using static iv' do
+ expect(Encryptor).to receive(:encrypt).with(described_class::AES256_GCM_OPTIONS.merge(value: 'some-value', iv: described_class::AES256_GCM_IV_STATIC)).and_return('hashed_value')
+
+ described_class.aes256_gcm_encrypt('some-value')
+ end
end
describe '.aes256_gcm_decrypt' do
- let(:encrypted) { described_class.aes256_gcm_encrypt('some-value') }
+ before do
+ stub_feature_flags(dynamic_nonce_creation: false)
+ end
+
+ context 'when token was encrypted using static nonce' do
+ let(:encrypted) { described_class.aes256_gcm_encrypt('some-value', nonce: described_class::AES256_GCM_IV_STATIC) }
+
+ it 'correctly decrypts encrypted string' do
+ decrypted = described_class.aes256_gcm_decrypt(encrypted)
+
+ expect(decrypted).to eq 'some-value'
+ end
+
+ it 'decrypts a value when it ends with a new line character' do
+ decrypted = described_class.aes256_gcm_decrypt(encrypted + "\n")
- it 'correctly decrypts encrypted string' do
- decrypted = described_class.aes256_gcm_decrypt(encrypted)
+ expect(decrypted).to eq 'some-value'
+ end
- expect(decrypted).to eq 'some-value'
+ it 'does not save hashed token with iv value in database' do
+ expect { described_class.aes256_gcm_decrypt(encrypted) }.not_to change { TokenWithIv.count }
+ end
+
+ context 'with feature flag switched on' do
+ before do
+ stub_feature_flags(dynamic_nonce_creation: true)
+ end
+
+ it 'correctly decrypts encrypted string' do
+ decrypted = described_class.aes256_gcm_decrypt(encrypted)
+
+ expect(decrypted).to eq 'some-value'
+ end
+ end
end
- it 'decrypts a value when it ends with a new line character' do
- decrypted = described_class.aes256_gcm_decrypt(encrypted + "\n")
+ context 'when token was encrypted using random nonce' do
+ let(:value) { 'random-value' }
+
+ # for compatibility with tokens encrypted using dynamic nonce
+ let!(:encrypted) do
+ iv = create_nonce
+ encrypted_token = described_class.create_encrypted_token(value, iv)
+ TokenWithIv.create!(hashed_token: Digest::SHA256.digest(encrypted_token), hashed_plaintext_token: Digest::SHA256.digest(encrypted_token), iv: iv)
+ encrypted_token
+ end
+
+ before do
+ stub_feature_flags(dynamic_nonce_creation: true)
+ end
- expect(decrypted).to eq 'some-value'
+ it 'correctly decrypts encrypted string' do
+ decrypted = described_class.aes256_gcm_decrypt(encrypted)
+
+ expect(decrypted).to eq value
+ end
+
+ it 'does not save hashed token with iv value in database' do
+ expect { described_class.aes256_gcm_decrypt(encrypted) }.not_to change { TokenWithIv.count }
+ end
end
end
+
+ def create_nonce
+ cipher = OpenSSL::Cipher.new('aes-256-gcm')
+ cipher.encrypt # Required before '#random_iv' can be called
+ cipher.random_iv # Ensures that the IV is the correct length respective to the algorithm used.
+ end
end
diff --git a/spec/lib/gitlab/current_settings_spec.rb b/spec/lib/gitlab/current_settings_spec.rb
index 786db23ffc4..01aceec12c5 100644
--- a/spec/lib/gitlab/current_settings_spec.rb
+++ b/spec/lib/gitlab/current_settings_spec.rb
@@ -194,4 +194,32 @@ RSpec.describe Gitlab::CurrentSettings do
end
end
end
+
+ describe '#current_application_settings?', :use_clean_rails_memory_store_caching do
+ before do
+ allow(Gitlab::CurrentSettings).to receive(:current_application_settings?).and_call_original
+ end
+
+ it 'returns true when settings exist' do
+ create(:application_setting,
+ home_page_url: 'http://mydomain.com',
+ signup_enabled: false)
+
+ expect(described_class.current_application_settings?).to eq(true)
+ end
+
+ it 'returns false when settings do not exist' do
+ expect(described_class.current_application_settings?).to eq(false)
+ end
+
+ context 'with cache', :request_store do
+ include_context 'with settings in cache'
+
+ it 'returns an in-memory ApplicationSetting object' do
+ expect(ApplicationSetting).not_to receive(:current)
+
+ expect(described_class.current_application_settings?).to eq(true)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb
index 21503dc1501..76578340f7b 100644
--- a/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb
@@ -218,7 +218,7 @@ RSpec.describe Gitlab::CycleAnalytics::StageSummary do
context 'when `to` is given' do
before do
- Timecop.freeze(5.days.from_now) { create(:deployment, :success, project: project) }
+ Timecop.freeze(5.days.from_now) { create(:deployment, :success, project: project, finished_at: Time.zone.now) }
end
it 'finds records created between `from` and `to` range' do
@@ -230,12 +230,34 @@ RSpec.describe Gitlab::CycleAnalytics::StageSummary do
end
context 'when `from` and `to` are within a day' do
- it 'returns the number of deployments made on that day' do
- freeze_time do
- create(:deployment, :success, project: project)
- options[:from] = options[:to] = Time.now
+ context 'when query_deploymenys_via_finished_at_in_vsa feature flag is off' do
+ before do
+ stub_feature_flags(query_deploymenys_via_finished_at_in_vsa: false)
+ end
+
+ it 'returns the number of deployments made on that day' do
+ freeze_time do
+ create(:deployment, :success, project: project)
+ options[:from] = options[:to] = Time.zone.now
+
+ expect(subject).to eq('1')
+ end
+ end
+ end
+
+ context 'when query_deploymenys_via_finished_at_in_vsa feature flag is off' do
+ before do
+ stub_feature_flags(query_deploymenys_via_finished_at_in_vsa: true)
+ end
+
+ it 'returns the number of deployments made on that day' do
+ freeze_time do
+ create(:deployment, :success, project: project, finished_at: Time.zone.now)
+ options[:from] = Time.zone.now.at_beginning_of_day
+ options[:to] = Time.zone.now.at_end_of_day
- expect(subject).to eq('1')
+ expect(subject).to eq('1')
+ end
end
end
end
diff --git a/spec/lib/gitlab/data_builder/build_spec.rb b/spec/lib/gitlab/data_builder/build_spec.rb
index 2f74e766a11..4242469b3db 100644
--- a/spec/lib/gitlab/data_builder/build_spec.rb
+++ b/spec/lib/gitlab/data_builder/build_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
RSpec.describe Gitlab::DataBuilder::Build do
- let(:runner) { create(:ci_runner, :instance) }
+ 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) }
let(:build) { create(:ci_build, :running, runner: runner, user: user) }
@@ -35,6 +36,7 @@ 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) }
context 'commit author_url' do
diff --git a/spec/lib/gitlab/data_builder/pipeline_spec.rb b/spec/lib/gitlab/data_builder/pipeline_spec.rb
index 297d87708d8..fd7cadeb89e 100644
--- a/spec/lib/gitlab/data_builder/pipeline_spec.rb
+++ b/spec/lib/gitlab/data_builder/pipeline_spec.rb
@@ -51,13 +51,15 @@ RSpec.describe Gitlab::DataBuilder::Pipeline do
context 'build with runner' do
let!(:build) { create(:ci_build, pipeline: pipeline, runner: ci_runner) }
- let(:ci_runner) { create(:ci_runner) }
+ let!(:tag_names) { %w(tag-1 tag-2) }
+ let(:ci_runner) { create(:ci_runner, tag_list: tag_names.map { |n| ActsAsTaggableOn::Tag.create!(name: n)}) }
it 'has runner attributes', :aggregate_failures do
expect(runner_data[:id]).to eq(ci_runner.id)
expect(runner_data[:description]).to eq(ci_runner.description)
expect(runner_data[:active]).to eq(ci_runner.active)
expect(runner_data[:is_shared]).to eq(ci_runner.instance_type?)
+ expect(runner_data[:tags]).to match_array(tag_names)
end
end
@@ -102,5 +104,16 @@ RSpec.describe Gitlab::DataBuilder::Pipeline do
expect(merge_request_attrs[:url]).to eq("http://localhost/#{merge_request.target_project.full_path}/-/merge_requests/#{merge_request.iid}")
end
end
+
+ context 'when pipeline has retried builds' do
+ before do
+ create(:ci_build, :retried, pipeline: pipeline)
+ end
+
+ it 'does not contain retried builds in payload' do
+ expect(data[:builds].count).to eq(1)
+ expect(build_data[:id]).to eq(build.id)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/database/migration_helpers/v2_spec.rb b/spec/lib/gitlab/database/migration_helpers/v2_spec.rb
new file mode 100644
index 00000000000..f132ecbf13b
--- /dev/null
+++ b/spec/lib/gitlab/database/migration_helpers/v2_spec.rb
@@ -0,0 +1,221 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::MigrationHelpers::V2 do
+ include Database::TriggerHelpers
+
+ let(:migration) do
+ ActiveRecord::Migration.new.extend(described_class)
+ end
+
+ before do
+ allow(migration).to receive(:puts)
+ end
+
+ shared_examples_for 'Setting up to rename a column' do
+ let(:model) { Class.new(ActiveRecord::Base) }
+
+ before do
+ model.table_name = :test_table
+ end
+
+ context 'when called inside a transaction block' do
+ before do
+ allow(migration).to receive(:transaction_open?).and_return(true)
+ end
+
+ it 'raises an error' do
+ expect do
+ migration.public_send(operation, :test_table, :original, :renamed)
+ end.to raise_error("#{operation} can not be run inside a transaction")
+ 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 '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
+ end
+
+ context 'when passing a batch column' do
+ context 'when the batch column does not exist' do
+ it 'raises an error' do
+ expect do
+ migration.public_send(operation, :test_table, :original, :renamed, batch_column_name: :missing)
+ end.to raise_error('Column missing does not exist on test_table')
+ end
+ end
+
+ context 'when the batch column does exist' do
+ it 'passes it when creating the column' do
+ expect(migration).to receive(:create_column_from)
+ .with(:test_table, existing_column, added_column, type: nil, batch_column_name: :status)
+ .and_call_original
+
+ migration.public_send(operation, :test_table, :original, :renamed, batch_column_name: :status)
+ end
+ 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)
+
+ 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')
+
+ 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')
+
+ new_record_1.update!(original: 'updated')
+ new_record_2.update!(renamed: nil)
+
+ 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)
+ end
+ end
+
+ describe '#rename_column_concurrently' do
+ before do
+ allow(migration).to receive(:transaction_open?).and_return(false)
+
+ migration.create_table :test_table do |t|
+ t.integer :status, null: false
+ t.text :original
+ t.text :other_column
+ end
+ end
+
+ it_behaves_like 'Setting up to rename a column' do
+ let(:operation) { :rename_column_concurrently }
+ let(:existing_column) { :original }
+ let(:added_column) { :renamed }
+ end
+
+ context 'when the column to rename does not exist' do
+ it 'raises an error' do
+ expect do
+ migration.rename_column_concurrently :test_table, :missing_column, :renamed
+ end.to raise_error('Column missing_column does not exist on test_table')
+ end
+ end
+ end
+
+ describe '#undo_cleanup_concurrent_column_rename' do
+ before do
+ allow(migration).to receive(:transaction_open?).and_return(false)
+
+ migration.create_table :test_table do |t|
+ t.integer :status, null: false
+ t.text :other_column
+ t.text :renamed
+ end
+ end
+
+ it_behaves_like 'Setting up to rename a column' do
+ let(:operation) { :undo_cleanup_concurrent_column_rename }
+ let(:existing_column) { :renamed }
+ let(:added_column) { :original }
+ end
+
+ context 'when the renamed column does not exist' do
+ it 'raises an error' do
+ expect do
+ migration.undo_cleanup_concurrent_column_rename :test_table, :original, :missing_column
+ end.to raise_error('Column missing_column does not exist on test_table')
+ end
+ end
+ end
+
+ shared_examples_for 'Cleaning up from renaming a column' do
+ let(:connection) { migration.connection }
+
+ before do
+ allow(migration).to receive(:transaction_open?).and_return(false)
+
+ migration.create_table :test_table do |t|
+ t.integer :status, null: false
+ t.text :original
+ t.text :other_column
+ end
+
+ migration.rename_column_concurrently :test_table, :original, :renamed
+ end
+
+ context 'when the helper is called repeatedly' do
+ before do
+ migration.public_send(operation, :test_table, :original, :renamed)
+ end
+
+ it 'does not make repeated attempts to cleanup' do
+ expect(migration).not_to receive(:remove_column)
+
+ expect do
+ migration.public_send(operation, :test_table, :original, :renamed)
+ end.not_to raise_error
+ end
+ end
+
+ context 'when the renamed column exists' do
+ let(:triggers) do
+ [
+ ['trigger_7cc71f92fd63', 'function_for_trigger_7cc71f92fd63', before: 'insert'],
+ ['trigger_f1a1f619636a', 'function_for_trigger_f1a1f619636a', before: 'update'],
+ ['trigger_769a49938884', 'function_for_trigger_769a49938884', before: 'update']
+ ]
+ end
+
+ it 'removes the sync triggers and renamed columns' do
+ triggers.each do |(trigger_name, function_name, event)|
+ expect_function_to_exist(function_name)
+ expect_valid_function_trigger(:test_table, trigger_name, function_name, event)
+ end
+
+ expect(migration.column_exists?(:test_table, added_column)).to eq(true)
+
+ migration.public_send(operation, :test_table, :original, :renamed)
+
+ expect(migration.column_exists?(:test_table, added_column)).to eq(false)
+
+ triggers.each do |(trigger_name, function_name, _)|
+ expect_trigger_not_to_exist(:test_table, trigger_name)
+ expect_function_not_to_exist(function_name)
+ end
+ end
+ end
+ end
+
+ describe '#undo_rename_column_concurrently' do
+ it_behaves_like 'Cleaning up from renaming a column' do
+ let(:operation) { :undo_rename_column_concurrently }
+ let(:added_column) { :renamed }
+ end
+ end
+
+ describe '#cleanup_concurrent_column_rename' do
+ it_behaves_like 'Cleaning up from renaming a column' do
+ let(:operation) { :cleanup_concurrent_column_rename }
+ let(:added_column) { :original }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 6b709cba5b3..6de7fc3a50e 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -1874,7 +1874,6 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
has_internal_id :iid,
scope: :project,
init: ->(s, _scope) { s&.project&.issues&.maximum(:iid) },
- backfill: true,
presence: false
end
end
@@ -1928,258 +1927,6 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
expect(issue_b.iid).to eq(3)
end
- context 'when the new code creates a row post deploy but before the migration runs' do
- it 'does not change the row iid' do
- project = setup
- issue = Issue.create!(project_id: project.id)
-
- model.backfill_iids('issues')
-
- expect(issue.reload.iid).to eq(1)
- end
-
- it 'backfills iids for rows already in the database' do
- project = setup
- issue_a = issues.create!(project_id: project.id)
- issue_b = issues.create!(project_id: project.id)
- issue_c = Issue.create!(project_id: project.id)
-
- model.backfill_iids('issues')
-
- expect(issue_a.reload.iid).to eq(1)
- expect(issue_b.reload.iid).to eq(2)
- expect(issue_c.reload.iid).to eq(3)
- end
-
- it 'backfills iids across multiple projects' do
- project_a = setup
- project_b = setup
- issue_a = issues.create!(project_id: project_a.id)
- issue_b = issues.create!(project_id: project_b.id)
- issue_c = Issue.create!(project_id: project_a.id)
- issue_d = Issue.create!(project_id: project_b.id)
-
- model.backfill_iids('issues')
-
- expect(issue_a.reload.iid).to eq(1)
- expect(issue_b.reload.iid).to eq(1)
- expect(issue_c.reload.iid).to eq(2)
- expect(issue_d.reload.iid).to eq(2)
- end
-
- it 'generates iids properly for models created after the migration' do
- project = setup
- issue_a = issues.create!(project_id: project.id)
- issue_b = issues.create!(project_id: project.id)
- issue_c = Issue.create!(project_id: project.id)
-
- model.backfill_iids('issues')
-
- issue_d = Issue.create!(project_id: project.id)
- issue_e = Issue.create!(project_id: project.id)
-
- expect(issue_a.reload.iid).to eq(1)
- expect(issue_b.reload.iid).to eq(2)
- expect(issue_c.reload.iid).to eq(3)
- expect(issue_d.iid).to eq(4)
- expect(issue_e.iid).to eq(5)
- end
-
- it 'backfills iids and properly generates iids for new models across multiple projects' do
- project_a = setup
- project_b = setup
- issue_a = issues.create!(project_id: project_a.id)
- issue_b = issues.create!(project_id: project_b.id)
- issue_c = Issue.create!(project_id: project_a.id)
- issue_d = Issue.create!(project_id: project_b.id)
-
- model.backfill_iids('issues')
-
- issue_e = Issue.create!(project_id: project_a.id)
- issue_f = Issue.create!(project_id: project_b.id)
- issue_g = Issue.create!(project_id: project_a.id)
-
- expect(issue_a.reload.iid).to eq(1)
- expect(issue_b.reload.iid).to eq(1)
- expect(issue_c.reload.iid).to eq(2)
- expect(issue_d.reload.iid).to eq(2)
- expect(issue_e.iid).to eq(3)
- expect(issue_f.iid).to eq(3)
- expect(issue_g.iid).to eq(4)
- end
- end
-
- context 'when the new code creates a model and then old code creates a model post deploy but before the migration runs' do
- it 'backfills iids' do
- project = setup
- issue_a = issues.create!(project_id: project.id)
- issue_b = Issue.create!(project_id: project.id)
- issue_c = issues.create!(project_id: project.id)
-
- model.backfill_iids('issues')
-
- expect(issue_a.reload.iid).to eq(1)
- expect(issue_b.reload.iid).to eq(2)
- expect(issue_c.reload.iid).to eq(3)
- end
-
- it 'generates an iid for a new model after the migration' do
- project = setup
- issue_a = issues.create!(project_id: project.id)
- issue_b = issues.create!(project_id: project.id)
- issue_c = Issue.create!(project_id: project.id)
- issue_d = issues.create!(project_id: project.id)
-
- model.backfill_iids('issues')
-
- issue_e = Issue.create!(project_id: project.id)
-
- expect(issue_a.reload.iid).to eq(1)
- expect(issue_b.reload.iid).to eq(2)
- expect(issue_c.reload.iid).to eq(3)
- expect(issue_d.reload.iid).to eq(4)
- expect(issue_e.iid).to eq(5)
- end
- end
-
- context 'when the new code and old code alternate creating models post deploy but before the migration runs' do
- it 'backfills iids' do
- project = setup
- issue_a = issues.create!(project_id: project.id)
- issue_b = Issue.create!(project_id: project.id)
- issue_c = issues.create!(project_id: project.id)
- issue_d = Issue.create!(project_id: project.id)
-
- model.backfill_iids('issues')
-
- expect(issue_a.reload.iid).to eq(1)
- expect(issue_b.reload.iid).to eq(2)
- expect(issue_c.reload.iid).to eq(3)
- expect(issue_d.reload.iid).to eq(4)
- end
-
- it 'generates an iid for a new model after the migration' do
- project = setup
- issue_a = issues.create!(project_id: project.id)
- issue_b = issues.create!(project_id: project.id)
- issue_c = Issue.create!(project_id: project.id)
- issue_d = issues.create!(project_id: project.id)
- issue_e = Issue.create!(project_id: project.id)
-
- model.backfill_iids('issues')
-
- issue_f = Issue.create!(project_id: project.id)
-
- expect(issue_a.reload.iid).to eq(1)
- expect(issue_b.reload.iid).to eq(2)
- expect(issue_c.reload.iid).to eq(3)
- expect(issue_d.reload.iid).to eq(4)
- expect(issue_e.reload.iid).to eq(5)
- expect(issue_f.iid).to eq(6)
- end
- end
-
- context 'when the new code creates and deletes a model post deploy but before the migration runs' do
- it 'backfills iids for rows already in the database' do
- project = setup
- issue_a = issues.create!(project_id: project.id)
- issue_b = issues.create!(project_id: project.id)
- issue_c = Issue.create!(project_id: project.id)
- issue_c.delete
-
- model.backfill_iids('issues')
-
- expect(issue_a.reload.iid).to eq(1)
- expect(issue_b.reload.iid).to eq(2)
- end
-
- it 'successfully creates a new model after the migration' do
- project = setup
- issue_a = issues.create!(project_id: project.id)
- issue_b = issues.create!(project_id: project.id)
- issue_c = Issue.create!(project_id: project.id)
- issue_c.delete
-
- model.backfill_iids('issues')
-
- issue_d = Issue.create!(project_id: project.id)
-
- expect(issue_a.reload.iid).to eq(1)
- expect(issue_b.reload.iid).to eq(2)
- expect(issue_d.iid).to eq(3)
- end
- end
-
- context 'when the new code creates and deletes a model and old code creates a model post deploy but before the migration runs' do
- it 'backfills iids' do
- project = setup
- issue_a = issues.create!(project_id: project.id)
- issue_b = issues.create!(project_id: project.id)
- issue_c = Issue.create!(project_id: project.id)
- issue_c.delete
- issue_d = issues.create!(project_id: project.id)
-
- model.backfill_iids('issues')
-
- expect(issue_a.reload.iid).to eq(1)
- expect(issue_b.reload.iid).to eq(2)
- expect(issue_d.reload.iid).to eq(3)
- end
-
- it 'successfully creates a new model after the migration' do
- project = setup
- issue_a = issues.create!(project_id: project.id)
- issue_b = issues.create!(project_id: project.id)
- issue_c = Issue.create!(project_id: project.id)
- issue_c.delete
- issue_d = issues.create!(project_id: project.id)
-
- model.backfill_iids('issues')
-
- issue_e = Issue.create!(project_id: project.id)
-
- expect(issue_a.reload.iid).to eq(1)
- expect(issue_b.reload.iid).to eq(2)
- expect(issue_d.reload.iid).to eq(3)
- expect(issue_e.iid).to eq(4)
- end
- end
-
- context 'when the new code creates and deletes a model and then creates another model post deploy but before the migration runs' do
- it 'successfully generates an iid for a new model after the migration' do
- project = setup
- issue_a = issues.create!(project_id: project.id)
- issue_b = issues.create!(project_id: project.id)
- issue_c = Issue.create!(project_id: project.id)
- issue_c.delete
- issue_d = Issue.create!(project_id: project.id)
-
- model.backfill_iids('issues')
-
- expect(issue_a.reload.iid).to eq(1)
- expect(issue_b.reload.iid).to eq(2)
- expect(issue_d.reload.iid).to eq(3)
- end
-
- it 'successfully generates an iid for a new model after the migration' do
- project = setup
- issue_a = issues.create!(project_id: project.id)
- issue_b = issues.create!(project_id: project.id)
- issue_c = Issue.create!(project_id: project.id)
- issue_c.delete
- issue_d = Issue.create!(project_id: project.id)
-
- model.backfill_iids('issues')
-
- issue_e = Issue.create!(project_id: project.id)
-
- expect(issue_a.reload.iid).to eq(1)
- expect(issue_b.reload.iid).to eq(2)
- expect(issue_d.reload.iid).to eq(3)
- expect(issue_e.iid).to eq(4)
- end
- end
-
context 'when the first model is created for a project after the migration' do
it 'generates an iid' do
project_a = setup
diff --git a/spec/lib/gitlab/database/migrations/instrumentation_spec.rb b/spec/lib/gitlab/database/migrations/instrumentation_spec.rb
new file mode 100644
index 00000000000..3804dc52a77
--- /dev/null
+++ b/spec/lib/gitlab/database/migrations/instrumentation_spec.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::Migrations::Instrumentation do
+ describe '#observe' do
+ subject { described_class.new }
+
+ let(:migration) { 1234 }
+
+ it 'executes the given block' do
+ expect { |b| subject.observe(migration, &b) }.to yield_control
+ end
+
+ context 'behavior with observers' do
+ subject { described_class.new(observers).observe(migration) {} }
+
+ let(:observers) { [observer] }
+ let(:observer) { instance_double('Gitlab::Database::Migrations::Observers::MigrationObserver', before: nil, after: nil, record: nil) }
+
+ it 'calls #before, #after, #record on given observers' do
+ expect(observer).to receive(:before).ordered
+ expect(observer).to receive(:after).ordered
+ expect(observer).to receive(:record).ordered do |observation|
+ expect(observation.migration).to eq(migration)
+ end
+
+ subject
+ end
+
+ it 'ignores errors coming from observers #before' do
+ expect(observer).to receive(:before).and_raise('some error')
+
+ subject
+ end
+
+ it 'ignores errors coming from observers #after' do
+ expect(observer).to receive(:after).and_raise('some error')
+
+ subject
+ end
+
+ it 'ignores errors coming from observers #record' do
+ expect(observer).to receive(:record).and_raise('some error')
+
+ subject
+ end
+ end
+
+ context 'on successful execution' do
+ subject { described_class.new.observe(migration) {} }
+
+ it 'records walltime' do
+ expect(subject.walltime).not_to be_nil
+ end
+
+ it 'records success' do
+ expect(subject.success).to be_truthy
+ end
+
+ it 'records the migration version' do
+ expect(subject.migration).to eq(migration)
+ end
+ end
+
+ context 'upon failure' do
+ subject { described_class.new.observe(migration) { raise 'something went wrong' } }
+
+ it 'raises the exception' do
+ expect { subject }.to raise_error(/something went wrong/)
+ end
+
+ context 'retrieving observations' do
+ subject { instance.observations.first }
+
+ before do
+ instance.observe(migration) { raise 'something went wrong' }
+ rescue
+ # ignore
+ end
+
+ let(:instance) { described_class.new }
+
+ it 'records walltime' do
+ expect(subject.walltime).not_to be_nil
+ end
+
+ it 'records failure' do
+ expect(subject.success).to be_falsey
+ end
+
+ it 'records the migration version' do
+ expect(subject.migration).to eq(migration)
+ end
+ end
+ end
+
+ context 'sequence of migrations with failures' do
+ subject { described_class.new }
+
+ let(:migration1) { double('migration1', call: nil) }
+ let(:migration2) { double('migration2', call: nil) }
+
+ it 'records observations for all migrations' do
+ subject.observe('migration1') {}
+ subject.observe('migration2') { raise 'something went wrong' } rescue nil
+
+ expect(subject.observations.size).to eq(2)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/migrations/observers/total_database_size_change_spec.rb b/spec/lib/gitlab/database/migrations/observers/total_database_size_change_spec.rb
new file mode 100644
index 00000000000..73466471944
--- /dev/null
+++ b/spec/lib/gitlab/database/migrations/observers/total_database_size_change_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::Migrations::Observers::TotalDatabaseSizeChange do
+ subject { described_class.new }
+
+ let(:observation) { Gitlab::Database::Migrations::Observation.new }
+ let(:connection) { ActiveRecord::Base.connection }
+ let(:query) { 'select pg_database_size(current_database())' }
+
+ it 'records the size change' do
+ expect(connection).to receive(:execute).with(query).once.and_return([{ 'pg_database_size' => 1024 }])
+ expect(connection).to receive(:execute).with(query).once.and_return([{ 'pg_database_size' => 256 }])
+
+ subject.before
+ subject.after
+ subject.record(observation)
+
+ expect(observation.total_database_size_change).to eq(256 - 1024)
+ end
+
+ context 'out of order calls' do
+ before do
+ allow(connection).to receive(:execute).with(query).and_return([{ 'pg_database_size' => 1024 }])
+ end
+
+ it 'does not record anything if before size is unknown' do
+ subject.after
+
+ expect { subject.record(observation) }.not_to change { observation.total_database_size_change }
+ end
+
+ it 'does not record anything if after size is unknown' do
+ subject.before
+
+ expect { subject.record(observation) }.not_to change { observation.total_database_size_change }
+ 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 b50e02c7043..b5d741fc5e9 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
@@ -513,6 +513,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
context 'finishing pending background migration jobs' do
let(:source_table_double) { double('table name') }
let(:raw_arguments) { [1, 50_000, source_table_double, partitioned_table, source_column] }
+ let(:background_job) { double('background job', args: ['background jobs', raw_arguments]) }
before do
allow(migration).to receive(:table_exists?).with(partitioned_table).and_return(true)
@@ -528,7 +529,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
expect(Gitlab::BackgroundMigration).to receive(:steal)
.with(described_class::MIGRATION_CLASS_NAME)
- .and_yield(raw_arguments)
+ .and_yield(background_job)
expect(source_table_double).to receive(:==).with(source_table.to_s)
diff --git a/spec/lib/gitlab/database/with_lock_retries_spec.rb b/spec/lib/gitlab/database/with_lock_retries_spec.rb
index 220ae705e71..563399ff0d9 100644
--- a/spec/lib/gitlab/database/with_lock_retries_spec.rb
+++ b/spec/lib/gitlab/database/with_lock_retries_spec.rb
@@ -54,6 +54,10 @@ RSpec.describe Gitlab::Database::WithLockRetries do
lock_fiber.resume # start the transaction and lock the table
end
+ after do
+ lock_fiber.resume if lock_fiber.alive?
+ end
+
context 'lock_fiber' do
it 'acquires lock successfully' do
check_exclusive_lock_query = """
diff --git a/spec/lib/gitlab/diff/char_diff_spec.rb b/spec/lib/gitlab/diff/char_diff_spec.rb
new file mode 100644
index 00000000000..e4e2a3ba050
--- /dev/null
+++ b/spec/lib/gitlab/diff/char_diff_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'diff_match_patch'
+
+RSpec.describe Gitlab::Diff::CharDiff do
+ let(:old_string) { "Helo \n Worlld" }
+ let(:new_string) { "Hello \n World" }
+
+ subject(:diff) { described_class.new(old_string, new_string) }
+
+ describe '#generate_diff' do
+ context 'when old string is nil' do
+ let(:old_string) { nil }
+
+ it 'does not raise an error' do
+ expect { subject.generate_diff }.not_to raise_error
+ end
+
+ it 'treats nil values as blank strings' do
+ changes = subject.generate_diff
+
+ expect(changes).to eq([
+ [:insert, "Hello \n World"]
+ ])
+ end
+ end
+
+ it 'generates an array of changes' do
+ changes = subject.generate_diff
+
+ expect(changes).to eq([
+ [:equal, "Hel"],
+ [:insert, "l"],
+ [:equal, "o \n Worl"],
+ [:delete, "l"],
+ [:equal, "d"]
+ ])
+ end
+ end
+
+ describe '#changed_ranges' do
+ subject { diff.changed_ranges }
+
+ context 'when old string is nil' do
+ let(:old_string) { nil }
+
+ it 'returns lists of changes' do
+ old_diffs, new_diffs = subject
+
+ expect(old_diffs).to eq([])
+ expect(new_diffs).to eq([0..12])
+ end
+ end
+
+ it 'returns ranges of changes' do
+ old_diffs, new_diffs = subject
+
+ expect(old_diffs).to eq([11..11])
+ expect(new_diffs).to eq([3..3])
+ end
+ end
+
+ describe '#to_html' do
+ it 'returns an HTML representation of the diff' do
+ subject.generate_diff
+
+ expect(subject.to_html).to eq(
+ '<span class="idiff">Hel</span>' \
+ '<span class="idiff addition">l</span>' \
+ "<span class=\"idiff\">o \n Worl</span>" \
+ '<span class="idiff deletion">l</span>' \
+ '<span class="idiff">d</span>'
+ )
+ end
+ end
+end
diff --git a/spec/lib/gitlab/diff/file_collection_sorter_spec.rb b/spec/lib/gitlab/diff/file_collection_sorter_spec.rb
index 8822fc55c6e..9ba9271cefc 100644
--- a/spec/lib/gitlab/diff/file_collection_sorter_spec.rb
+++ b/spec/lib/gitlab/diff/file_collection_sorter_spec.rb
@@ -5,11 +5,14 @@ require 'spec_helper'
RSpec.describe Gitlab::Diff::FileCollectionSorter do
let(:diffs) do
[
+ double(new_path: 'README', old_path: 'README'),
double(new_path: '.dir/test', old_path: '.dir/test'),
double(new_path: '', old_path: '.file'),
double(new_path: '1-folder/A-file.ext', old_path: '1-folder/A-file.ext'),
+ double(new_path: '1-folder/README', old_path: '1-folder/README'),
double(new_path: nil, old_path: '1-folder/M-file.ext'),
double(new_path: '1-folder/Z-file.ext', old_path: '1-folder/Z-file.ext'),
+ double(new_path: '1-folder/README', old_path: '1-folder/README'),
double(new_path: '', old_path: '1-folder/nested/A-file.ext'),
double(new_path: '1-folder/nested/M-file.ext', old_path: '1-folder/nested/M-file.ext'),
double(new_path: nil, old_path: '1-folder/nested/Z-file.ext'),
@@ -19,7 +22,8 @@ RSpec.describe Gitlab::Diff::FileCollectionSorter do
double(new_path: nil, old_path: '2-folder/nested/A-file.ext'),
double(new_path: 'A-file.ext', old_path: 'A-file.ext'),
double(new_path: '', old_path: 'M-file.ext'),
- double(new_path: 'Z-file.ext', old_path: 'Z-file.ext')
+ double(new_path: 'Z-file.ext', old_path: 'Z-file.ext'),
+ double(new_path: 'README', old_path: 'README')
]
end
@@ -36,6 +40,8 @@ RSpec.describe Gitlab::Diff::FileCollectionSorter do
'1-folder/nested/Z-file.ext',
'1-folder/A-file.ext',
'1-folder/M-file.ext',
+ '1-folder/README',
+ '1-folder/README',
'1-folder/Z-file.ext',
'2-folder/nested/A-file.ext',
'2-folder/A-file.ext',
@@ -44,6 +50,8 @@ RSpec.describe Gitlab::Diff::FileCollectionSorter do
'.file',
'A-file.ext',
'M-file.ext',
+ 'README',
+ 'README',
'Z-file.ext'
])
end
diff --git a/spec/lib/gitlab/diff/highlight_cache_spec.rb b/spec/lib/gitlab/diff/highlight_cache_spec.rb
index f6810d7a966..94717152488 100644
--- a/spec/lib/gitlab/diff/highlight_cache_spec.rb
+++ b/spec/lib/gitlab/diff/highlight_cache_spec.rb
@@ -233,4 +233,22 @@ RSpec.describe Gitlab::Diff::HighlightCache, :clean_gitlab_redis_cache do
cache.write_if_empty
end
end
+
+ describe '#key' do
+ subject { cache.key }
+
+ it 'returns the next version of the cache' do
+ is_expected.to start_with("highlighted-diff-files:#{cache.diffable.cache_key}:2")
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(improved_merge_diff_highlighting: false)
+ end
+
+ it 'returns the original version of the cache' do
+ is_expected.to start_with("highlighted-diff-files:#{cache.diffable.cache_key}:1")
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/diff/inline_diff_spec.rb b/spec/lib/gitlab/diff/inline_diff_spec.rb
index 35284e952f7..dce655d5690 100644
--- a/spec/lib/gitlab/diff/inline_diff_spec.rb
+++ b/spec/lib/gitlab/diff/inline_diff_spec.rb
@@ -37,6 +37,33 @@ RSpec.describe Gitlab::Diff::InlineDiff do
it 'can handle unchanged empty lines' do
expect { described_class.for_lines(['- bar', '+ baz', '']) }.not_to raise_error
end
+
+ context 'when lines have multiple changes' do
+ let(:diff) do
+ <<~EOF
+ - Hello, how are you?
+ + Hi, how are you doing?
+ EOF
+ end
+
+ let(:subject) { described_class.for_lines(diff.lines) }
+
+ it 'finds all inline diffs' do
+ expect(subject[0]).to eq([3..6])
+ expect(subject[1]).to eq([3..3, 17..22])
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(improved_merge_diff_highlighting: false)
+ end
+
+ it 'finds all inline diffs' do
+ expect(subject[0]).to eq([3..19])
+ expect(subject[1]).to eq([3..22])
+ end
+ end
+ end
end
describe "#inline_diffs" do
diff --git a/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb b/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb
index b1ffbedc7bf..eb11c051adc 100644
--- a/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb
@@ -40,6 +40,13 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler do
expect(new_issue.description).to eq(expected_description.strip)
end
+ it 'creates an issue_email_participant' do
+ receiver.execute
+ new_issue = Issue.last
+
+ expect(new_issue.issue_email_participants.first.email).to eq("jake@adventuretime.ooo")
+ end
+
it 'sends thank you email' do
expect { receiver.execute }.to have_enqueued_job.on_queue('mailers')
end
diff --git a/spec/lib/gitlab/experimentation/controller_concern_spec.rb b/spec/lib/gitlab/experimentation/controller_concern_spec.rb
index c47f71c207d..1cebe37bea5 100644
--- a/spec/lib/gitlab/experimentation/controller_concern_spec.rb
+++ b/spec/lib/gitlab/experimentation/controller_concern_spec.rb
@@ -10,6 +10,10 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
use_backwards_compatible_subject_index: true
},
test_experiment: {
+ tracking_category: 'Team',
+ rollout_strategy: rollout_strategy
+ },
+ my_experiment: {
tracking_category: 'Team'
}
}
@@ -20,6 +24,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
end
let(:enabled_percentage) { 10 }
+ let(:rollout_strategy) { nil }
controller(ApplicationController) do
include Gitlab::Experimentation::ControllerConcern
@@ -117,6 +122,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
end
context 'when subject is given' do
+ let(:rollout_strategy) { :user }
let(:user) { build(:user) }
it 'uses the subject' do
@@ -244,6 +250,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
it "provides the subject's hashed global_id as label" do
experiment_subject = double(:subject, to_global_id: 'abc')
+ allow(Gitlab::Experimentation).to receive(:valid_subject_for_rollout_strategy?).and_return(true)
controller.track_experiment_event(:test_experiment, 'start', 1, subject: experiment_subject)
@@ -420,6 +427,26 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
controller.record_experiment_user(:test_experiment, context)
end
+
+ context 'with a cookie based rollout strategy' do
+ it 'calls tracking_group with a nil subject' do
+ expect(controller).to receive(:tracking_group).with(:test_experiment, nil, subject: nil).and_return(:experimental)
+ allow(::Experiment).to receive(:add_user).with(:test_experiment, :experimental, user, context)
+
+ controller.record_experiment_user(:test_experiment, context)
+ end
+ end
+
+ context 'with a user based rollout strategy' do
+ let(:rollout_strategy) { :user }
+
+ it 'calls tracking_group with a user subject' do
+ expect(controller).to receive(:tracking_group).with(:test_experiment, nil, subject: user).and_return(:experimental)
+ allow(::Experiment).to receive(:add_user).with(:test_experiment, :experimental, user, context)
+
+ controller.record_experiment_user(:test_experiment, context)
+ end
+ end
end
context 'the user is part of the control group' do
diff --git a/spec/lib/gitlab/experimentation/experiment_spec.rb b/spec/lib/gitlab/experimentation/experiment_spec.rb
index 008e6699597..94dbf1d7e4b 100644
--- a/spec/lib/gitlab/experimentation/experiment_spec.rb
+++ b/spec/lib/gitlab/experimentation/experiment_spec.rb
@@ -9,7 +9,8 @@ RSpec.describe Gitlab::Experimentation::Experiment do
let(:params) do
{
tracking_category: 'Category1',
- use_backwards_compatible_subject_index: true
+ use_backwards_compatible_subject_index: true,
+ rollout_strategy: nil
}
end
diff --git a/spec/lib/gitlab/experimentation_spec.rb b/spec/lib/gitlab/experimentation_spec.rb
index b503960b8c7..7eeae3f3f33 100644
--- a/spec/lib/gitlab/experimentation_spec.rb
+++ b/spec/lib/gitlab/experimentation_spec.rb
@@ -7,7 +7,6 @@ require 'spec_helper'
RSpec.describe Gitlab::Experimentation::EXPERIMENTS do
it 'temporarily ensures we know what experiments exist for backwards compatibility' do
expected_experiment_keys = [
- :onboarding_issues,
:ci_notification_dot,
:upgrade_link_in_user_menu_a,
:invite_members_version_a,
@@ -15,8 +14,7 @@ RSpec.describe Gitlab::Experimentation::EXPERIMENTS do
:invite_members_empty_group_version_a,
:contact_sales_btn_in_app,
:customize_homepage,
- :group_only_trials,
- :default_to_issues_board
+ :group_only_trials
]
backwards_compatible_experiment_keys = described_class.filter { |_, v| v[:use_backwards_compatible_subject_index] }.keys
@@ -27,6 +25,8 @@ RSpec.describe Gitlab::Experimentation::EXPERIMENTS do
end
RSpec.describe Gitlab::Experimentation do
+ using RSpec::Parameterized::TableSyntax
+
before do
stub_const('Gitlab::Experimentation::EXPERIMENTS', {
backwards_compatible_test_experiment: {
@@ -35,6 +35,10 @@ RSpec.describe Gitlab::Experimentation do
},
test_experiment: {
tracking_category: 'Team'
+ },
+ tabular_experiment: {
+ tracking_category: 'Team',
+ rollout_strategy: rollout_strategy
}
})
@@ -46,6 +50,7 @@ RSpec.describe Gitlab::Experimentation do
end
let(:enabled_percentage) { 10 }
+ let(:rollout_strategy) { nil }
describe '.get_experiment' do
subject { described_class.get_experiment(:test_experiment) }
@@ -175,4 +180,59 @@ RSpec.describe Gitlab::Experimentation do
end
end
end
+
+ describe '.log_invalid_rollout' do
+ subject { described_class.log_invalid_rollout(:test_experiment, 1) }
+
+ before do
+ allow(described_class).to receive(:valid_subject_for_rollout_strategy?).and_return(valid)
+ end
+
+ context 'subject is not valid for experiment' do
+ let(:valid) { false }
+
+ it 'logs a warning message' do
+ expect_next_instance_of(Gitlab::ExperimentationLogger) do |logger|
+ expect(logger)
+ .to receive(:warn)
+ .with(
+ message: 'Subject must conform to the rollout strategy',
+ experiment_key: :test_experiment,
+ subject: 'Integer',
+ rollout_strategy: :cookie
+ )
+ end
+
+ subject
+ end
+ end
+
+ context 'subject is valid for experiment' do
+ let(:valid) { true }
+
+ it 'does not log a warning message' do
+ expect(Gitlab::ExperimentationLogger).not_to receive(:build)
+
+ subject
+ end
+ end
+ end
+
+ describe '.valid_subject_for_rollout_strategy?' do
+ subject { described_class.valid_subject_for_rollout_strategy?(:tabular_experiment, experiment_subject) }
+
+ where(:rollout_strategy, :experiment_subject, :result) do
+ :cookie | nil | true
+ nil | nil | true
+ :cookie | 'string' | true
+ nil | User.new | false
+ :user | User.new | true
+ :group | User.new | false
+ :group | Group.new | true
+ end
+
+ with_them do
+ it { is_expected.to be(result) }
+ end
+ end
end
diff --git a/spec/lib/gitlab/file_finder_spec.rb b/spec/lib/gitlab/file_finder_spec.rb
index 8d6df62b3f6..0b5303f22b4 100644
--- a/spec/lib/gitlab/file_finder_spec.rb
+++ b/spec/lib/gitlab/file_finder_spec.rb
@@ -53,6 +53,14 @@ RSpec.describe Gitlab::FileFinder do
end
end
+ context 'with white space in the path' do
+ it 'filters by path correctly' do
+ results = subject.find('directory path:"with space/README.md"')
+
+ expect(results.count).to eq(1)
+ end
+ end
+
it 'does not cause N+1 query' do
expect(Gitlab::GitalyClient).to receive(:call).at_most(10).times.and_call_original
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index 8961cdcae7d..49f1e6e994f 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -720,7 +720,8 @@ RSpec.describe Gitlab::Git::Commit, :seed_helper do
committer_name: "Dmitriy Zaporozhets",
id: SeedRepo::Commit::ID,
message: "tree css fixes",
- parent_ids: ["874797c3a73b60d2187ed6e2fcabd289ff75171e"]
+ parent_ids: ["874797c3a73b60d2187ed6e2fcabd289ff75171e"],
+ trailers: {}
}
end
end
diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb
index 783f0a9ccf7..17bb83d0f2f 100644
--- a/spec/lib/gitlab/git/diff_spec.rb
+++ b/spec/lib/gitlab/git/diff_spec.rb
@@ -100,6 +100,13 @@ EOT
expect(diff.diff).to be_empty
expect(diff).to be_too_large
end
+
+ it 'logs the event' do
+ expect(Gitlab::Metrics).to receive(:add_event)
+ .with(:patch_hard_limit_bytes_hit)
+
+ diff
+ end
end
context 'using a collapsable diff that is too large' do
diff --git a/spec/lib/gitlab/git/push_spec.rb b/spec/lib/gitlab/git/push_spec.rb
index 0f52f10c0a6..8ba43b2967c 100644
--- a/spec/lib/gitlab/git/push_spec.rb
+++ b/spec/lib/gitlab/git/push_spec.rb
@@ -86,6 +86,16 @@ RSpec.describe Gitlab::Git::Push do
it { is_expected.to be_force_push }
end
+
+ context 'when called muiltiple times' do
+ it 'does not make make multiple calls to the force push check' do
+ expect(Gitlab::Checks::ForcePush).to receive(:force_push?).once
+
+ 2.times do
+ subject.force_push?
+ end
+ end
+ end
end
describe '#branch_added?' do
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index ef9b5a30c86..cc1b1ceadcf 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -1894,8 +1894,11 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
it 'removes the remote' do
repository_rugged.remotes.create(remote_name, url)
- repository.remove_remote(remote_name)
+ expect(repository.remove_remote(remote_name)).to be true
+ # Since we deleted the remote via Gitaly, Rugged doesn't know
+ # this changed underneath it. Let's refresh the Rugged repo.
+ repository_rugged = Rugged::Repository.new(repository_path)
expect(repository_rugged.remotes[remote_name]).to be_nil
end
end
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index a0cafe3d763..9a1ecfe6459 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -388,108 +388,6 @@ RSpec.describe Gitlab::GitAccess do
end
end
- describe '#check_otp_session!' do
- let_it_be(:user) { create(:user, :two_factor_via_otp)}
- let_it_be(:key) { create(:key, user: user) }
- let_it_be(:actor) { key }
-
- before do
- project.add_developer(user)
- stub_feature_flags(two_factor_for_cli: true)
- end
-
- context 'with an OTP session', :clean_gitlab_redis_shared_state do
- before do
- Gitlab::Redis::SharedState.with do |redis|
- redis.set("#{Gitlab::Auth::Otp::SessionEnforcer::OTP_SESSIONS_NAMESPACE}:#{key.id}", true)
- end
- end
-
- it 'allows push and pull access' do
- aggregate_failures do
- expect { push_access_check }.not_to raise_error
- expect { pull_access_check }.not_to raise_error
- end
- end
- end
-
- context 'without OTP session' do
- it 'does not allow push or pull access' do
- user = 'jane.doe'
- host = 'fridge.ssh'
- port = 42
-
- stub_config(
- gitlab_shell: {
- ssh_user: user,
- ssh_host: host,
- ssh_port: port
- }
- )
-
- error_message = "OTP verification is required to access the repository.\n\n"\
- " Use: ssh #{user}@#{host} -p #{port} 2fa_verify"
-
- aggregate_failures do
- expect { push_access_check }.to raise_forbidden(error_message)
- expect { pull_access_check }.to raise_forbidden(error_message)
- end
- end
-
- context 'when protocol is HTTP' do
- let(:protocol) { 'http' }
-
- it 'allows push and pull access' do
- aggregate_failures do
- expect { push_access_check }.not_to raise_error
- expect { pull_access_check }.not_to raise_error
- end
- end
- end
-
- context 'when actor is not an SSH key' do
- let(:deploy_key) { create(:deploy_key, user: user) }
- let(:actor) { deploy_key }
-
- before do
- deploy_key.deploy_keys_projects.create(project: project, can_push: true)
- end
-
- it 'allows push and pull access' do
- aggregate_failures do
- expect { push_access_check }.not_to raise_error
- expect { pull_access_check }.not_to raise_error
- end
- end
- end
-
- context 'when 2FA is not enabled for the user' do
- let(:user) { create(:user)}
- let(:actor) { create(:key, user: user) }
-
- it 'allows push and pull access' do
- aggregate_failures do
- expect { push_access_check }.not_to raise_error
- expect { pull_access_check }.not_to raise_error
- end
- end
- end
-
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(two_factor_for_cli: false)
- end
-
- it 'allows push and pull access' do
- aggregate_failures do
- expect { push_access_check }.not_to raise_error
- expect { pull_access_check }.not_to raise_error
- end
- end
- end
- end
- end
-
describe '#check_db_accessibility!' do
context 'when in a read-only GitLab instance' do
before do
diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
index ce01566b870..22707c9a36b 100644
--- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
@@ -299,6 +299,11 @@ RSpec.describe Gitlab::GitalyClient::OperationService do
let(:start_sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
let(:end_sha) { '54cec5282aa9f21856362fe321c800c236a61615' }
let(:commit_message) { 'Squash message' }
+
+ let(:time) do
+ Time.now.utc
+ end
+
let(:request) do
Gitaly::UserSquashRequest.new(
repository: repository.gitaly_repository,
@@ -307,7 +312,8 @@ RSpec.describe Gitlab::GitalyClient::OperationService do
start_sha: start_sha,
end_sha: end_sha,
author: gitaly_user,
- commit_message: commit_message
+ commit_message: commit_message,
+ timestamp: Google::Protobuf::Timestamp.new(seconds: time.to_i)
)
end
@@ -315,7 +321,7 @@ RSpec.describe Gitlab::GitalyClient::OperationService do
let(:response) { Gitaly::UserSquashResponse.new(squash_sha: squash_sha) }
subject do
- client.user_squash(user, squash_id, start_sha, end_sha, user, commit_message)
+ client.user_squash(user, squash_id, start_sha, end_sha, user, commit_message, time)
end
it 'sends a user_squash message and returns the squash sha' do
diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb
index 7fcb11c4dfd..a8d42f4bccf 100644
--- a/spec/lib/gitlab/gitaly_client_spec.rb
+++ b/spec/lib/gitlab/gitaly_client_spec.rb
@@ -267,31 +267,63 @@ RSpec.describe Gitlab::GitalyClient do
end
describe '.request_kwargs' do
- context 'when catfile-cache feature is enabled' do
- before do
- stub_feature_flags('gitaly_catfile-cache': true)
+ it 'sets the gitaly-session-id in the metadata' do
+ results = described_class.request_kwargs('default', timeout: 1)
+ expect(results[:metadata]).to include('gitaly-session-id')
+ end
+
+ context 'when RequestStore is not enabled' do
+ it 'sets a different gitaly-session-id per request' do
+ gitaly_session_id = described_class.request_kwargs('default', timeout: 1)[:metadata]['gitaly-session-id']
+
+ expect(described_class.request_kwargs('default', timeout: 1)[:metadata]['gitaly-session-id']).not_to eq(gitaly_session_id)
end
+ end
- it 'sets the gitaly-session-id in the metadata' do
- results = described_class.request_kwargs('default', timeout: 1)
- expect(results[:metadata]).to include('gitaly-session-id')
+ context 'when RequestStore is enabled', :request_store do
+ it 'sets the same gitaly-session-id on every outgoing request metadata' do
+ gitaly_session_id = described_class.request_kwargs('default', timeout: 1)[:metadata]['gitaly-session-id']
+
+ 3.times do
+ expect(described_class.request_kwargs('default', timeout: 1)[:metadata]['gitaly-session-id']).to eq(gitaly_session_id)
+ end
end
+ end
- context 'when RequestStore is not enabled' do
- it 'sets a different gitaly-session-id per request' do
- gitaly_session_id = described_class.request_kwargs('default', timeout: 1)[:metadata]['gitaly-session-id']
+ context 'gitlab_git_env' do
+ let(:policy) { 'gitaly-route-repository-accessor-policy' }
- expect(described_class.request_kwargs('default', timeout: 1)[:metadata]['gitaly-session-id']).not_to eq(gitaly_session_id)
+ context 'when RequestStore is disabled' do
+ it 'does not force-route to primary' do
+ expect(described_class.request_kwargs('default', timeout: 1)[:metadata][policy]).to be_nil
end
end
- context 'when RequestStore is enabled', :request_store do
- it 'sets the same gitaly-session-id on every outgoing request metadata' do
- gitaly_session_id = described_class.request_kwargs('default', timeout: 1)[:metadata]['gitaly-session-id']
+ context 'when RequestStore is enabled without git_env', :request_store do
+ it 'does not force-orute to primary' do
+ expect(described_class.request_kwargs('default', timeout: 1)[:metadata][policy]).to be_nil
+ end
+ end
- 3.times do
- expect(described_class.request_kwargs('default', timeout: 1)[:metadata]['gitaly-session-id']).to eq(gitaly_session_id)
- end
+ context 'when RequestStore is enabled with empty git_env', :request_store do
+ before do
+ Gitlab::SafeRequestStore[:gitlab_git_env] = {}
+ end
+
+ it 'disables force-routing to primary' do
+ expect(described_class.request_kwargs('default', timeout: 1)[:metadata][policy]).to be_nil
+ end
+ end
+
+ context 'when RequestStore is enabled with populated git_env', :request_store do
+ before do
+ Gitlab::SafeRequestStore[:gitlab_git_env] = {
+ "GIT_OBJECT_DIRECTORY_RELATIVE" => "foo/bar"
+ }
+ end
+
+ it 'enables force-routing to primary' do
+ expect(described_class.request_kwargs('default', timeout: 1)[:metadata][policy]).to eq('primary-only')
end
end
end
diff --git a/spec/lib/gitlab/graphql/pagination/connections_spec.rb b/spec/lib/gitlab/graphql/pagination/connections_spec.rb
new file mode 100644
index 00000000000..e89e5c17644
--- /dev/null
+++ b/spec/lib/gitlab/graphql/pagination/connections_spec.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+# Tests that our connections are correctly mapped.
+RSpec.describe ::Gitlab::Graphql::Pagination::Connections do
+ include GraphqlHelpers
+
+ before(:all) do
+ ActiveRecord::Schema.define do
+ create_table :testing_pagination_nodes, force: true do |t|
+ t.integer :value, null: false
+ end
+ end
+ end
+
+ after(:all) do
+ ActiveRecord::Schema.define do
+ drop_table :testing_pagination_nodes, force: true
+ end
+ end
+
+ let_it_be(:node_model) do
+ Class.new(ActiveRecord::Base) do
+ self.table_name = 'testing_pagination_nodes'
+ end
+ end
+
+ let(:query_string) { 'query { items(first: 2) { nodes { value } } }' }
+ let(:user) { nil }
+
+ let(:node) { Struct.new(:value) }
+ let(:node_type) do
+ Class.new(::GraphQL::Schema::Object) do
+ graphql_name 'Node'
+ field :value, GraphQL::INT_TYPE, null: false
+ end
+ end
+
+ let(:query_type) do
+ item_values = nodes
+
+ query_factory do |t|
+ t.field :items, node_type.connection_type, null: true
+
+ t.define_method :items do
+ item_values
+ end
+ end
+ end
+
+ shared_examples 'it maps to a specific connection class' do |connection_type|
+ let(:raw_values) { [1, 7, 42] }
+
+ it "maps to #{connection_type.name}" do
+ expect(connection_type).to receive(:new).and_call_original
+
+ results = execute_query(query_type).to_h
+
+ expect(graphql_dig_at(results, :data, :items, :nodes, :value)).to eq [1, 7]
+ end
+ end
+
+ describe 'OffsetPaginatedRelation' do
+ before do
+ # Expect to be ordered by an explicit ordering.
+ raw_values.each_with_index { |value, id| node_model.create!(id: id, value: value) }
+ end
+
+ let(:nodes) { ::Gitlab::Graphql::Pagination::OffsetPaginatedRelation.new(node_model.order(value: :asc)) }
+
+ include_examples 'it maps to a specific connection class', Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection
+ end
+
+ describe 'ActiveRecord::Relation' do
+ before do
+ # Expect to be ordered by ID descending
+ [3, 2, 1].zip(raw_values) { |id, value| node_model.create!(id: id, value: value) }
+ end
+
+ let(:nodes) { node_model.all }
+
+ include_examples 'it maps to a specific connection class', Gitlab::Graphql::Pagination::Keyset::Connection
+ end
+
+ describe 'ExternallyPaginatedArray' do
+ let(:nodes) { ::Gitlab::Graphql::ExternallyPaginatedArray.new(nil, nil, node.new(1), node.new(7)) }
+
+ include_examples 'it maps to a specific connection class', Gitlab::Graphql::Pagination::ExternallyPaginatedArrayConnection
+ end
+
+ describe 'Array' do
+ let(:nodes) { raw_values.map { |x| node.new(x) } }
+
+ include_examples 'it maps to a specific connection class', Gitlab::Graphql::Pagination::ArrayConnection
+ end
+end
diff --git a/spec/lib/gitlab/graphql/queries_spec.rb b/spec/lib/gitlab/graphql/queries_spec.rb
index 6e08a87523f..a140a283c1b 100644
--- a/spec/lib/gitlab/graphql/queries_spec.rb
+++ b/spec/lib/gitlab/graphql/queries_spec.rb
@@ -151,6 +151,10 @@ RSpec.describe Gitlab::Graphql::Queries do
let(:path) { 'post_by_slug.graphql' }
it_behaves_like 'a valid GraphQL query for the blog schema'
+
+ it 'has a complexity' do
+ expect(subject.complexity(schema)).to be < 10
+ end
end
context 'a query with an import' do
diff --git a/spec/lib/gitlab/health_checks/master_check_spec.rb b/spec/lib/gitlab/health_checks/master_check_spec.rb
index 1c1efe178e2..287ebcec207 100644
--- a/spec/lib/gitlab/health_checks/master_check_spec.rb
+++ b/spec/lib/gitlab/health_checks/master_check_spec.rb
@@ -4,47 +4,67 @@ require 'spec_helper'
require_relative './simple_check_shared'
RSpec.describe Gitlab::HealthChecks::MasterCheck do
- let(:result_class) { Gitlab::HealthChecks::Result }
-
before do
stub_const('SUCCESS_CODE', 100)
stub_const('FAILURE_CODE', 101)
- described_class.register_master
end
- after do
- described_class.finish_master
- end
+ context 'when Puma runs in Clustered mode' do
+ before do
+ allow(Gitlab::Runtime).to receive(:puma_in_clustered_mode?).and_return(true)
- describe '#readiness' do
- context 'when master is running' do
- it 'worker does return success' do
- _, child_status = run_worker
+ described_class.register_master
+ end
- expect(child_status.exitstatus).to eq(SUCCESS_CODE)
- end
+ after do
+ described_class.finish_master
end
- context 'when master finishes early' do
- before do
- described_class.send(:close_write)
+ describe '.available?' do
+ specify { expect(described_class.available?).to be true }
+ end
+
+ describe '.readiness' do
+ context 'when master is running' do
+ it 'worker does return success' do
+ _, child_status = run_worker
+
+ expect(child_status.exitstatus).to eq(SUCCESS_CODE)
+ end
end
- it 'worker does return failure' do
- _, child_status = run_worker
+ context 'when master finishes early' do
+ before do
+ described_class.send(:close_write)
+ end
- expect(child_status.exitstatus).to eq(FAILURE_CODE)
+ it 'worker does return failure' do
+ _, child_status = run_worker
+
+ expect(child_status.exitstatus).to eq(FAILURE_CODE)
+ end
end
- end
- def run_worker
- pid = fork do
- described_class.register_worker
+ def run_worker
+ pid = fork do
+ described_class.register_worker
- exit(described_class.readiness.success ? SUCCESS_CODE : FAILURE_CODE)
+ exit(described_class.readiness.success ? SUCCESS_CODE : FAILURE_CODE)
+ end
+
+ Process.wait2(pid)
end
+ end
+ end
+
+ # '.readiness' check is not invoked if '.available?' returns false
+ context 'when Puma runs in Single mode' do
+ before do
+ allow(Gitlab::Runtime).to receive(:puma_in_clustered_mode?).and_return(false)
+ end
- Process.wait2(pid)
+ describe '.available?' do
+ specify { expect(described_class.available?).to be false }
end
end
end
diff --git a/spec/lib/gitlab/health_checks/probes/collection_spec.rb b/spec/lib/gitlab/health_checks/probes/collection_spec.rb
index 03138e936aa..69828c143db 100644
--- a/spec/lib/gitlab/health_checks/probes/collection_spec.rb
+++ b/spec/lib/gitlab/health_checks/probes/collection_spec.rb
@@ -61,6 +61,35 @@ RSpec.describe Gitlab::HealthChecks::Probes::Collection do
expect(subject.json[:message]).to eq('Redis::CannotConnectError : Redis down')
end
end
+
+ context 'when some checks are not available' do
+ before do
+ allow(Gitlab::Runtime).to receive(:puma_in_clustered_mode?).and_return(false)
+ end
+
+ let(:checks) do
+ [
+ Gitlab::HealthChecks::MasterCheck
+ ]
+ end
+
+ it 'asks for check availability' do
+ expect(Gitlab::HealthChecks::MasterCheck).to receive(:available?)
+
+ subject
+ end
+
+ it 'does not call `readiness` on checks that are not available' do
+ expect(Gitlab::HealthChecks::MasterCheck).not_to receive(:readiness)
+
+ subject
+ end
+
+ it 'does not fail collection check' do
+ expect(subject.http_status).to eq(200)
+ expect(subject.json[:status]).to eq('ok')
+ end
+ end
end
context 'without checks' do
diff --git a/spec/lib/gitlab/hook_data/group_builder_spec.rb b/spec/lib/gitlab/hook_data/group_builder_spec.rb
new file mode 100644
index 00000000000..d7347ff99d4
--- /dev/null
+++ b/spec/lib/gitlab/hook_data/group_builder_spec.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::HookData::GroupBuilder do
+ let_it_be(:group) { create(:group) }
+
+ describe '#build' do
+ let(:data) { described_class.new(group).build(event) }
+ let(:event_name) { data[:event_name] }
+ let(:attributes) do
+ [
+ :event_name, :created_at, :updated_at, :name, :path, :full_path, :group_id
+ ]
+ end
+
+ context 'data' do
+ shared_examples_for 'includes the required attributes' do
+ it 'includes the required attributes' do
+ expect(data).to include(*attributes)
+
+ expect(data[:name]).to eq(group.name)
+ expect(data[:path]).to eq(group.path)
+ expect(data[:full_path]).to eq(group.full_path)
+ expect(data[:group_id]).to eq(group.id)
+ expect(data[:created_at]).to eq(group.created_at.xmlschema)
+ expect(data[:updated_at]).to eq(group.updated_at.xmlschema)
+ end
+ end
+
+ shared_examples_for 'does not include old path attributes' do
+ it 'does not include old path attributes' do
+ expect(data).not_to include(:old_path, :old_full_path)
+ end
+ end
+
+ context 'on create' do
+ let(:event) { :create }
+
+ it { expect(event_name).to eq('group_create') }
+ it_behaves_like 'includes the required attributes'
+ it_behaves_like 'does not include old path attributes'
+ end
+
+ context 'on destroy' do
+ let(:event) { :destroy }
+
+ it { expect(event_name).to eq('group_destroy') }
+ it_behaves_like 'includes the required attributes'
+ it_behaves_like 'does not include old path attributes'
+ end
+
+ context 'on rename' do
+ let(:event) { :rename }
+
+ it { expect(event_name).to eq('group_rename') }
+ it_behaves_like 'includes the required attributes'
+
+ it 'includes old path details' do
+ allow(group).to receive(:path_before_last_save).and_return('old-path')
+
+ expect(data[:old_path]).to eq(group.path_before_last_save)
+ expect(data[:old_full_path]).to eq(group.path_before_last_save)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/hook_data/subgroup_builder_spec.rb b/spec/lib/gitlab/hook_data/subgroup_builder_spec.rb
new file mode 100644
index 00000000000..89e5dffd7b4
--- /dev/null
+++ b/spec/lib/gitlab/hook_data/subgroup_builder_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::HookData::SubgroupBuilder do
+ let_it_be(:parent_group) { create(:group) }
+ let_it_be(:subgroup) { create(:group, parent: parent_group) }
+
+ describe '#build' do
+ let(:data) { described_class.new(subgroup).build(event) }
+ let(:event_name) { data[:event_name] }
+ let(:attributes) do
+ [
+ :event_name, :created_at, :updated_at, :name, :path, :full_path, :group_id,
+ :parent_group_id, :parent_name, :parent_path, :parent_full_path
+ ]
+ end
+
+ context 'data' do
+ shared_examples_for 'includes the required attributes' do
+ it 'includes the required attributes' do
+ expect(data).to include(*attributes)
+
+ expect(data[:name]).to eq(subgroup.name)
+ expect(data[:path]).to eq(subgroup.path)
+ expect(data[:full_path]).to eq(subgroup.full_path)
+ expect(data[:group_id]).to eq(subgroup.id)
+ expect(data[:created_at]).to eq(subgroup.created_at.xmlschema)
+ expect(data[:updated_at]).to eq(subgroup.updated_at.xmlschema)
+ expect(data[:parent_name]).to eq(parent_group.name)
+ expect(data[:parent_path]).to eq(parent_group.path)
+ expect(data[:parent_full_path]).to eq(parent_group.full_path)
+ expect(data[:parent_group_id]).to eq(parent_group.id)
+ end
+ end
+
+ context 'on create' do
+ let(:event) { :create }
+
+ it { expect(event_name).to eq('subgroup_create') }
+ it_behaves_like 'includes the required attributes'
+ end
+
+ context 'on destroy' do
+ let(:event) { :destroy }
+
+ it { expect(event_name).to eq('subgroup_destroy') }
+ it_behaves_like 'includes the required attributes'
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 825513bdfc5..d0282e14d5f 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -146,6 +146,7 @@ merge_requests:
- merge_user
- merge_request_diffs
- merge_request_diff
+- merge_head_diff
- merge_request_context_commits
- merge_request_context_commit_diff_files
- events
@@ -544,7 +545,7 @@ project:
- daily_build_group_report_results
- jira_imports
- compliance_framework_setting
-- compliance_management_frameworks
+- compliance_management_framework
- metrics_users_starred_dashboards
- alert_management_alerts
- repository_storage_moves
@@ -560,7 +561,10 @@ project:
- alert_management_http_integrations
- exported_protected_branches
- incident_management_oncall_schedules
+- incident_management_oncall_rotations
- debian_distributions
+- merge_request_metrics
+- security_orchestration_policy_configuration
award_emoji:
- awardable
- user
@@ -589,6 +593,7 @@ lfs_file_locks:
project_badges:
- project
metrics:
+- target_project
- merge_request
- latest_closed_by
- merged_by
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 efb271086a0..96c467e78d6 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
@@ -27,25 +27,55 @@ RSpec.describe Gitlab::ImportExport::DecompressedArchiveSizeValidator do
end
context 'when file exceeds allowed decompressed size' do
- it 'returns false' do
+ it 'logs error message returns false' do
+ expect(Gitlab::Import::Logger)
+ .to receive(:info)
+ .with(
+ import_upload_archive_path: filepath,
+ import_upload_archive_size: File.size(filepath),
+ message: 'Decompressed archive size limit reached'
+ )
expect(subject.valid?).to eq(false)
end
end
- context 'when something goes wrong during decompression' do
- before do
- allow(subject.archive_file).to receive(:eof?).and_raise(StandardError)
+ context 'when exception occurs during decompression' do
+ shared_examples 'logs raised exception and terminates validator process group' do
+ let(:std) { double(:std, close: nil, value: nil) }
+ let(:wait_thr) { double }
+
+ before do
+ allow(Process).to receive(:getpgid).and_return(2)
+ allow(Open3).to receive(:popen3).and_return([std, std, std, wait_thr])
+ allow(wait_thr).to receive(:[]).with(:pid).and_return(1)
+ allow(wait_thr).to receive(:value).and_raise(exception)
+ end
+
+ it 'logs raised exception and terminates validator process group' do
+ expect(Gitlab::Import::Logger)
+ .to receive(:info)
+ .with(
+ import_upload_archive_path: filepath,
+ import_upload_archive_size: File.size(filepath),
+ message: error_message
+ )
+ expect(Process).to receive(:kill).with(-1, 2)
+ expect(subject.valid?).to eq(false)
+ end
end
- it 'logs and tracks raised exception' do
- expect(Gitlab::ErrorTracking).to receive(:track_exception).with(instance_of(StandardError))
- expect(Gitlab::Import::Logger).to receive(:info).with(hash_including(message: 'Decompressed archive size validation failed.'))
+ context 'when timeout occurs' do
+ let(:error_message) { 'Timeout reached during archive decompression' }
+ let(:exception) { Timeout::Error }
- subject.valid?
+ include_examples 'logs raised exception and terminates validator process group'
end
- it 'returns false' do
- expect(subject.valid?).to eq(false)
+ context 'when exception occurs' do
+ let(:error_message) { 'Error!' }
+ let(:exception) { StandardError.new(error_message) }
+
+ include_examples 'logs raised exception and terminates validator process group'
end
end
end
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 b311a02833c..6680f4e7a03 100644
--- a/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb
@@ -11,12 +11,12 @@ RSpec.describe Gitlab::ImportExport::DesignRepoRestorer do
let!(:project) { create(:project) }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:shared) { project.import_export_shared }
- let(:bundler) { Gitlab::ImportExport::DesignRepoSaver.new(project: project_with_design_repo, shared: shared) }
+ let(:bundler) { Gitlab::ImportExport::DesignRepoSaver.new(exportable: project_with_design_repo, shared: shared) }
let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.design_repo_bundle_filename) }
let(:restorer) do
described_class.new(path_to_bundle: bundle_path,
shared: shared,
- project: project)
+ importable: project)
end
before do
diff --git a/spec/lib/gitlab/import_export/design_repo_saver_spec.rb b/spec/lib/gitlab/import_export/design_repo_saver_spec.rb
index 2575d209db5..5501e3dee5a 100644
--- a/spec/lib/gitlab/import_export/design_repo_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/design_repo_saver_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Gitlab::ImportExport::DesignRepoSaver do
let!(:project) { create(:project, :design_repo) }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:shared) { project.import_export_shared }
- let(:design_bundler) { described_class.new(project: project, shared: shared) }
+ let(:design_bundler) { described_class.new(exportable: project, shared: shared) }
before do
project.add_maintainer(user)
diff --git a/spec/lib/gitlab/import_export/fork_spec.rb b/spec/lib/gitlab/import_export/fork_spec.rb
index ef7394053b9..65c28a8b8a2 100644
--- a/spec/lib/gitlab/import_export/fork_spec.rb
+++ b/spec/lib/gitlab/import_export/fork_spec.rb
@@ -12,11 +12,11 @@ RSpec.describe 'forked project import' do
let(:shared) { project.import_export_shared }
let(:forked_from_project) { create(:project, :repository) }
let(:forked_project) { fork_project(project_with_repo, nil, repository: true) }
- let(:repo_saver) { Gitlab::ImportExport::RepoSaver.new(project: project_with_repo, shared: shared) }
+ let(:repo_saver) { Gitlab::ImportExport::RepoSaver.new(exportable: project_with_repo, shared: shared) }
let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) }
let(:repo_restorer) do
- Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: bundle_path, shared: shared, project: project)
+ Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: bundle_path, shared: shared, importable: project)
end
let!(:merge_request) do
diff --git a/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb
index 2794acb8980..d2153221e8f 100644
--- a/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb
@@ -21,6 +21,7 @@ RSpec.describe Gitlab::ImportExport::Group::TreeRestorer do
group_tree_restorer = described_class.new(user: user, shared: @shared, group: @group)
expect(group_tree_restorer.restore).to be_truthy
+ expect(group_tree_restorer.groups_mapping).not_to be_empty
end
end
diff --git a/spec/lib/gitlab/import_export/importer_spec.rb b/spec/lib/gitlab/import_export/importer_spec.rb
index 75db3167ebc..20f0f6af6f3 100644
--- a/spec/lib/gitlab/import_export/importer_spec.rb
+++ b/spec/lib/gitlab/import_export/importer_spec.rb
@@ -69,8 +69,8 @@ RSpec.describe Gitlab::ImportExport::Importer do
repo_path = File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename)
restorer = double(Gitlab::ImportExport::RepoRestorer)
- expect(Gitlab::ImportExport::RepoRestorer).to receive(:new).with(path_to_bundle: repo_path, shared: shared, project: project).and_return(restorer)
- expect(Gitlab::ImportExport::RepoRestorer).to receive(:new).with(path_to_bundle: wiki_repo_path, shared: shared, project: ProjectWiki.new(project)).and_return(restorer)
+ expect(Gitlab::ImportExport::RepoRestorer).to receive(:new).with(path_to_bundle: repo_path, shared: shared, importable: project).and_return(restorer)
+ expect(Gitlab::ImportExport::RepoRestorer).to receive(:new).with(path_to_bundle: wiki_repo_path, shared: shared, importable: ProjectWiki.new(project)).and_return(restorer)
expect(Gitlab::ImportExport::RepoRestorer).to receive(:new).and_call_original
expect(restorer).to receive(:restore).and_return(true).twice
diff --git a/spec/lib/gitlab/import_export/repo_restorer_spec.rb b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
index a6b917457c2..fe43a23e242 100644
--- a/spec/lib/gitlab/import_export/repo_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
@@ -27,10 +27,10 @@ RSpec.describe Gitlab::ImportExport::RepoRestorer do
end
describe 'bundle a project Git repo' do
- let(:bundler) { Gitlab::ImportExport::RepoSaver.new(project: project_with_repo, shared: shared) }
+ let(:bundler) { Gitlab::ImportExport::RepoSaver.new(exportable: project_with_repo, shared: shared) }
let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) }
- subject { described_class.new(path_to_bundle: bundle_path, shared: shared, project: project) }
+ subject { described_class.new(path_to_bundle: bundle_path, shared: shared, importable: project) }
after do
Gitlab::Shell.new.remove_repository(project.repository_storage, project.disk_path)
@@ -62,10 +62,10 @@ RSpec.describe Gitlab::ImportExport::RepoRestorer do
end
describe 'restore a wiki Git repo' do
- let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(project: project_with_repo, shared: shared) }
+ let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(exportable: project_with_repo, shared: shared) }
let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.wiki_repo_bundle_filename) }
- subject { described_class.new(path_to_bundle: bundle_path, shared: shared, project: ProjectWiki.new(project)) }
+ subject { described_class.new(path_to_bundle: bundle_path, shared: shared, importable: ProjectWiki.new(project)) }
after do
Gitlab::Shell.new.remove_repository(project.wiki.repository_storage, project.wiki.disk_path)
@@ -83,7 +83,7 @@ RSpec.describe Gitlab::ImportExport::RepoRestorer do
describe 'no wiki in the bundle' do
let!(:project_without_wiki) { create(:project) }
- let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(project: project_without_wiki, shared: shared) }
+ let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(exportable: project_without_wiki, shared: shared) }
it 'does not creates an empty wiki' do
expect(subject.restore).to be true
diff --git a/spec/lib/gitlab/import_export/repo_saver_spec.rb b/spec/lib/gitlab/import_export/repo_saver_spec.rb
index 73d51000c67..52001e778d6 100644
--- a/spec/lib/gitlab/import_export/repo_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/repo_saver_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Gitlab::ImportExport::RepoSaver do
let!(:project) { create(:project, :repository) }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:shared) { project.import_export_shared }
- let(:bundler) { described_class.new(project: project, shared: shared) }
+ let(:bundler) { described_class.new(exportable: project, shared: shared) }
before do
project.add_maintainer(user)
@@ -25,6 +25,14 @@ RSpec.describe Gitlab::ImportExport::RepoSaver do
expect(bundler.save).to be true
end
+ it 'creates the directory for the repository' do
+ allow(bundler).to receive(:bundle_full_path).and_return('/foo/bar/file.tar.gz')
+
+ expect(FileUtils).to receive(:mkdir_p).with('/foo/bar', anything)
+
+ bundler.save # rubocop:disable Rails/SaveBang
+ end
+
context 'when the repo is empty' do
let!(:project) { create(:project) }
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index a93ee051ccf..e301be47d68 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -220,6 +220,7 @@ MergeRequestDiff:
- commits_count
- files_count
- sorted
+- diff_type
MergeRequestDiffCommit:
- merge_request_diff_id
- relative_order
@@ -231,6 +232,7 @@ MergeRequestDiffCommit:
- committer_name
- committer_email
- message
+- trailers
MergeRequestDiffFile:
- merge_request_diff_id
- relative_order
@@ -255,6 +257,7 @@ MergeRequestContextCommit:
- committer_email
- message
- merge_request_id
+- trailers
MergeRequestContextCommitDiffFile:
- sha
- relative_order
@@ -580,6 +583,7 @@ ProjectFeature:
- requirements_access_level
- analytics_access_level
- operations_access_level
+- security_and_compliance_access_level
- created_at
- updated_at
ProtectedBranch::MergeAccessLevel:
diff --git a/spec/lib/gitlab/import_export/saver_spec.rb b/spec/lib/gitlab/import_export/saver_spec.rb
index 865c7e57b5a..877474dd862 100644
--- a/spec/lib/gitlab/import_export/saver_spec.rb
+++ b/spec/lib/gitlab/import_export/saver_spec.rb
@@ -6,7 +6,8 @@ require 'fileutils'
RSpec.describe Gitlab::ImportExport::Saver do
let!(:project) { create(:project, :public, name: 'project') }
let(:base_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
- let(:export_path) { "#{base_path}/project_tree_saver_spec/export" }
+ let(:archive_path) { "#{base_path}/archive" }
+ let(:export_path) { "#{archive_path}/export" }
let(:shared) { project.import_export_shared }
subject { described_class.new(exportable: project, shared: shared) }
@@ -35,10 +36,13 @@ RSpec.describe Gitlab::ImportExport::Saver do
.to match(%r[\/uploads\/-\/system\/import_export_upload\/export_file.*])
end
- it 'removes tmp files' do
+ it 'removes archive path and keeps base path untouched' do
+ allow(shared).to receive(:archive_path).and_return(archive_path)
+
subject.save
- expect(FileUtils).to have_received(:rm_rf).with(base_path)
- expect(Dir.exist?(base_path)).to eq(false)
+ expect(FileUtils).not_to have_received(:rm_rf).with(base_path)
+ expect(FileUtils).to have_received(:rm_rf).with(archive_path)
+ expect(Dir.exist?(archive_path)).to eq(false)
end
end
diff --git a/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb b/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb
index 778d0859bf1..540f90e7804 100644
--- a/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Gitlab::ImportExport::WikiRepoSaver do
let_it_be(:project) { create(:project, :wiki_repo) }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:shared) { project.import_export_shared }
- let(:wiki_bundler) { described_class.new(project: project, shared: shared) }
+ let(:wiki_bundler) { described_class.new(exportable: project, shared: shared) }
let!(:project_wiki) { ProjectWiki.new(project, user) }
before do
diff --git a/spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb b/spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb
index 2ca7465e775..e4af3f77d5d 100644
--- a/spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb
+++ b/spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb
@@ -53,6 +53,7 @@ RSpec.describe Gitlab::Instrumentation::RedisClusterValidator do
:del | [%w(foo bar)] | true # Arguments can be a nested array
:del | %w(foo foo) | false
:hset | %w(foo bar) | false # Not a multi-key command
+ :mget | [] | false # This is invalid, but not because it's a cross-slot command
end
with_them do
diff --git a/spec/lib/gitlab/instrumentation_helper_spec.rb b/spec/lib/gitlab/instrumentation_helper_spec.rb
index c00b0fdf043..a5c9cde4c37 100644
--- a/spec/lib/gitlab/instrumentation_helper_spec.rb
+++ b/spec/lib/gitlab/instrumentation_helper_spec.rb
@@ -9,12 +9,17 @@ RSpec.describe Gitlab::InstrumentationHelper do
describe '.keys' do
it 'returns all available payload keys' do
expected_keys = [
+ :cpu_s,
:gitaly_calls,
:gitaly_duration_s,
:rugged_calls,
:rugged_duration_s,
:elasticsearch_calls,
:elasticsearch_duration_s,
+ :elasticsearch_timed_out_count,
+ :mem_objects,
+ :mem_bytes,
+ :mem_mallocs,
:redis_calls,
:redis_duration_s,
:redis_read_bytes,
@@ -37,7 +42,11 @@ RSpec.describe Gitlab::InstrumentationHelper do
:redis_shared_state_write_bytes,
:db_count,
:db_write_count,
- :db_cached_count
+ :db_cached_count,
+ :external_http_count,
+ :external_http_duration_s,
+ :rack_attack_redis_count,
+ :rack_attack_redis_duration_s
]
expect(described_class.keys).to eq(expected_keys)
@@ -49,10 +58,14 @@ RSpec.describe Gitlab::InstrumentationHelper do
subject { described_class.add_instrumentation_data(payload) }
- it 'adds only DB counts by default' do
+ before do
+ described_class.init_instrumentation_data
+ end
+
+ it 'includes DB counts' do
subject
- expect(payload).to eq(db_count: 0, db_cached_count: 0, db_write_count: 0)
+ expect(payload).to include(db_count: 0, db_cached_count: 0, db_write_count: 0)
end
context 'when Gitaly calls are made' do
@@ -110,6 +123,47 @@ RSpec.describe Gitlab::InstrumentationHelper do
expect(payload[:throttle_safelist]).to eq('foobar')
end
end
+
+ it 'logs cpu_s duration' do
+ subject
+
+ expect(payload).to include(:cpu_s)
+ end
+
+ context 'when logging memory allocations' do
+ include MemoryInstrumentationHelper
+
+ before do
+ skip_memory_instrumentation!
+ end
+
+ it 'logs memory usage metrics' do
+ subject
+
+ expect(payload).to include(
+ :mem_objects,
+ :mem_bytes,
+ :mem_mallocs
+ )
+ end
+
+ context 'when trace_memory_allocations is disabled' do
+ before do
+ stub_feature_flags(trace_memory_allocations: false)
+ Gitlab::Memory::Instrumentation.ensure_feature_flag!
+ end
+
+ it 'does not log memory usage metrics' do
+ subject
+
+ expect(payload).not_to include(
+ :mem_objects,
+ :mem_bytes,
+ :mem_mallocs
+ )
+ end
+ end
+ end
end
describe '.queue_duration_for_job' do
diff --git a/spec/lib/gitlab/kas_spec.rb b/spec/lib/gitlab/kas_spec.rb
index ce22f36e9fd..01ced407883 100644
--- a/spec/lib/gitlab/kas_spec.rb
+++ b/spec/lib/gitlab/kas_spec.rb
@@ -58,4 +58,48 @@ RSpec.describe Gitlab::Kas do
end
end
end
+
+ describe '.included_in_gitlab_com_rollout?' do
+ let_it_be(:project) { create(:project) }
+
+ context 'not GitLab.com' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(false)
+ end
+
+ it 'returns true' do
+ expect(described_class.included_in_gitlab_com_rollout?(project)).to be_truthy
+ end
+ end
+
+ context 'GitLab.com' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ context 'kubernetes_agent_on_gitlab_com feature flag disabled' do
+ before do
+ stub_feature_flags(kubernetes_agent_on_gitlab_com: false)
+ end
+
+ it 'returns false' do
+ expect(described_class.included_in_gitlab_com_rollout?(project)).to be_falsey
+ end
+ end
+
+ context 'kubernetes_agent_on_gitlab_com feature flag enabled' do
+ before do
+ stub_feature_flags(kubernetes_agent_on_gitlab_com: project)
+ end
+
+ it 'returns true' do
+ expect(described_class.included_in_gitlab_com_rollout?(project)).to be_truthy
+ end
+
+ it 'returns false for another project' do
+ expect(described_class.included_in_gitlab_com_rollout?(create(:project))).to be_falsey
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/memory/instrumentation_spec.rb b/spec/lib/gitlab/memory/instrumentation_spec.rb
new file mode 100644
index 00000000000..6b53550a3d0
--- /dev/null
+++ b/spec/lib/gitlab/memory/instrumentation_spec.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Memory::Instrumentation do
+ include MemoryInstrumentationHelper
+
+ before do
+ skip_memory_instrumentation!
+ end
+
+ describe '.available?' do
+ it 'returns true' do
+ expect(described_class).to be_available
+ end
+ end
+
+ describe '.start_thread_memory_allocations' do
+ subject { described_class.start_thread_memory_allocations }
+
+ context 'when feature flag trace_memory_allocations is enabled' do
+ before do
+ stub_feature_flags(trace_memory_allocations: true)
+ end
+
+ it 'a hash is returned' do
+ is_expected.not_to be_empty
+ end
+ end
+
+ context 'when feature flag trace_memory_allocations is disabled' do
+ before do
+ stub_feature_flags(trace_memory_allocations: false)
+ end
+
+ it 'a nil is returned' do
+ is_expected.to be_nil
+ end
+ end
+
+ context 'when feature is unavailable' do
+ before do
+ allow(described_class).to receive(:available?) { false }
+ end
+
+ it 'a nil is returned' do
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ describe '.with_memory_allocations' do
+ let(:ntimes) { 100 }
+
+ subject do
+ described_class.with_memory_allocations do
+ Array.new(1000).map { '0' * 100 }
+ end
+ end
+
+ before do
+ expect(described_class).to receive(:start_thread_memory_allocations).and_call_original
+ expect(described_class).to receive(:measure_thread_memory_allocations).and_call_original
+ end
+
+ context 'when feature flag trace_memory_allocations is enabled' do
+ before do
+ stub_feature_flags(trace_memory_allocations: true)
+ end
+
+ it 'a hash is returned' do
+ is_expected.to include(
+ mem_objects: be > 1000,
+ mem_mallocs: be > 1000,
+ mem_bytes: be > 100_000 # 100 items * 100 bytes each
+ )
+ end
+ end
+
+ context 'when feature flag trace_memory_allocations is disabled' do
+ before do
+ stub_feature_flags(trace_memory_allocations: false)
+ end
+
+ it 'a nil is returned' do
+ is_expected.to be_nil
+ end
+ end
+
+ context 'when feature is unavailable' do
+ before do
+ allow(described_class).to receive(:available?) { false }
+ end
+
+ it 'a nil is returned' do
+ is_expected.to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/subscribers/external_http_spec.rb b/spec/lib/gitlab/metrics/subscribers/external_http_spec.rb
new file mode 100644
index 00000000000..5bcaf8fbc47
--- /dev/null
+++ b/spec/lib/gitlab/metrics/subscribers/external_http_spec.rb
@@ -0,0 +1,172 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Metrics::Subscribers::ExternalHttp, :request_store do
+ let(:transaction) { Gitlab::Metrics::Transaction.new }
+ let(:subscriber) { described_class.new }
+
+ let(:event_1) do
+ double(:event, payload: {
+ method: 'POST', code: "200", duration: 0.321,
+ scheme: 'https', host: 'gitlab.com', port: 80, path: '/api/v4/projects',
+ query: 'current=true'
+ })
+ end
+
+ let(:event_2) do
+ double(:event, payload: {
+ method: 'GET', code: "301", duration: 0.12,
+ scheme: 'http', host: 'gitlab.com', port: 80, path: '/api/v4/projects/2',
+ query: 'current=true'
+ })
+ end
+
+ let(:event_3) do
+ double(:event, payload: {
+ method: 'POST', duration: 5.3,
+ scheme: 'http', host: 'gitlab.com', port: 80, path: '/api/v4/projects/2/issues',
+ query: 'current=true',
+ exception_object: Net::ReadTimeout.new
+ })
+ end
+
+ describe '.detail_store' do
+ context 'when external HTTP detail store is empty' do
+ before do
+ Gitlab::SafeRequestStore[:peek_enabled] = true
+ end
+
+ it 'returns an empty array' do
+ expect(described_class.detail_store).to eql([])
+ end
+ end
+
+ context 'when the performance bar is not enabled' do
+ it 'returns an empty array' do
+ expect(described_class.detail_store).to eql([])
+ end
+ end
+
+ context 'when external HTTP detail store has some values' do
+ before do
+ Gitlab::SafeRequestStore[:peek_enabled] = true
+ Gitlab::SafeRequestStore[:external_http_detail_store] = [{
+ method: 'POST', code: "200", duration: 0.321
+ }]
+ end
+
+ it 'returns the external http detailed store' do
+ expect(described_class.detail_store).to eql([{ method: 'POST', code: "200", duration: 0.321 }])
+ end
+ end
+ end
+
+ describe '.payload' do
+ context 'when SafeRequestStore does not have any item from external HTTP' do
+ it 'returns an empty array' do
+ expect(described_class.payload).to eql(external_http_count: 0, external_http_duration_s: 0.0)
+ end
+ end
+
+ context 'when external HTTP recorded some values' do
+ before do
+ Gitlab::SafeRequestStore[:external_http_count] = 7
+ Gitlab::SafeRequestStore[:external_http_duration_s] = 1.2
+ end
+
+ it 'returns the external http detailed store' do
+ expect(described_class.payload).to eql(external_http_count: 7, external_http_duration_s: 1.2)
+ end
+ end
+ end
+
+ describe '#request' do
+ before do
+ Gitlab::SafeRequestStore[:peek_enabled] = true
+ allow(subscriber).to receive(:current_transaction).and_return(transaction)
+ end
+
+ it 'tracks external HTTP request count' do
+ expect(transaction).to receive(:increment)
+ .with(:gitlab_external_http_total, 1, { code: "200", method: "POST" })
+ expect(transaction).to receive(:increment)
+ .with(:gitlab_external_http_total, 1, { code: "301", method: "GET" })
+
+ subscriber.request(event_1)
+ subscriber.request(event_2)
+ end
+
+ it 'tracks external HTTP duration' do
+ expect(transaction).to receive(:observe)
+ .with(:gitlab_external_http_duration_seconds, 0.321)
+ expect(transaction).to receive(:observe)
+ .with(:gitlab_external_http_duration_seconds, 0.12)
+ expect(transaction).to receive(:observe)
+ .with(:gitlab_external_http_duration_seconds, 5.3)
+
+ subscriber.request(event_1)
+ subscriber.request(event_2)
+ subscriber.request(event_3)
+ end
+
+ it 'tracks external HTTP exceptions' do
+ expect(transaction).to receive(:increment)
+ .with(:gitlab_external_http_total, 1, { code: 'undefined', method: "POST" })
+ expect(transaction).to receive(:increment)
+ .with(:gitlab_external_http_exception_total, 1)
+
+ subscriber.request(event_3)
+ end
+
+ it 'stores per-request counters' do
+ subscriber.request(event_1)
+ subscriber.request(event_2)
+ subscriber.request(event_3)
+
+ expect(Gitlab::SafeRequestStore[:external_http_count]).to eq(3)
+ expect(Gitlab::SafeRequestStore[:external_http_duration_s]).to eq(5.741) # 0.321 + 0.12 + 5.3
+ end
+
+ it 'stores a portion of events into the detail store' do
+ subscriber.request(event_1)
+ subscriber.request(event_2)
+ subscriber.request(event_3)
+
+ expect(Gitlab::SafeRequestStore[:external_http_detail_store].length).to eq(3)
+ expect(Gitlab::SafeRequestStore[:external_http_detail_store][0]).to include(
+ method: 'POST', code: "200", duration: 0.321,
+ scheme: 'https', host: 'gitlab.com', port: 80, path: '/api/v4/projects',
+ query: 'current=true', exception_object: nil,
+ backtrace: be_a(Array)
+ )
+ expect(Gitlab::SafeRequestStore[:external_http_detail_store][1]).to include(
+ method: 'GET', code: "301", duration: 0.12,
+ scheme: 'http', host: 'gitlab.com', port: 80, path: '/api/v4/projects/2',
+ query: 'current=true', exception_object: nil,
+ backtrace: be_a(Array)
+ )
+ expect(Gitlab::SafeRequestStore[:external_http_detail_store][2]).to include(
+ method: 'POST', duration: 5.3,
+ scheme: 'http', host: 'gitlab.com', port: 80, path: '/api/v4/projects/2/issues',
+ query: 'current=true',
+ exception_object: be_a(Net::ReadTimeout),
+ backtrace: be_a(Array)
+ )
+ end
+
+ context 'when the performance bar is not enabled' do
+ before do
+ Gitlab::SafeRequestStore.delete(:peek_enabled)
+ end
+
+ it 'does not capture detail store' do
+ subscriber.request(event_1)
+ subscriber.request(event_2)
+ subscriber.request(event_3)
+
+ expect(Gitlab::SafeRequestStore[:external_http_detail_store]).to be(nil)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/subscribers/rack_attack_spec.rb b/spec/lib/gitlab/metrics/subscribers/rack_attack_spec.rb
new file mode 100644
index 00000000000..2d595632772
--- /dev/null
+++ b/spec/lib/gitlab/metrics/subscribers/rack_attack_spec.rb
@@ -0,0 +1,203 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Metrics::Subscribers::RackAttack, :request_store do
+ let(:subscriber) { described_class.new }
+
+ describe '.payload' do
+ context 'when the request store is empty' do
+ it 'returns empty data' do
+ expect(described_class.payload).to eql(
+ rack_attack_redis_count: 0,
+ rack_attack_redis_duration_s: 0.0
+ )
+ end
+ end
+
+ context 'when the request store already has data' do
+ before do
+ Gitlab::SafeRequestStore[:rack_attack_instrumentation] = {
+ rack_attack_redis_count: 10,
+ rack_attack_redis_duration_s: 9.0
+ }
+ end
+
+ it 'returns the accumulated data' do
+ expect(described_class.payload).to eql(
+ rack_attack_redis_count: 10,
+ rack_attack_redis_duration_s: 9.0
+ )
+ end
+ end
+ end
+
+ describe '#redis' do
+ it 'accumulates per-request RackAttack cache usage' do
+ freeze_time do
+ subscriber.redis(
+ ActiveSupport::Notifications::Event.new(
+ 'redis.rack_attack', Time.current, Time.current + 1.second, '1', { operation: 'fetch' }
+ )
+ )
+ subscriber.redis(
+ ActiveSupport::Notifications::Event.new(
+ 'redis.rack_attack', Time.current, Time.current + 2.seconds, '1', { operation: 'write' }
+ )
+ )
+ subscriber.redis(
+ ActiveSupport::Notifications::Event.new(
+ 'redis.rack_attack', Time.current, Time.current + 3.seconds, '1', { operation: 'read' }
+ )
+ )
+ end
+
+ expect(Gitlab::SafeRequestStore[:rack_attack_instrumentation]).to eql(
+ rack_attack_redis_count: 3,
+ rack_attack_redis_duration_s: 6.0
+ )
+ end
+ end
+
+ shared_examples 'log into auth logger' do
+ context 'when matched throttle does not require user information' do
+ let(:event) do
+ ActiveSupport::Notifications::Event.new(
+ event_name, Time.current, Time.current + 2.seconds, '1', request: double(
+ :request,
+ ip: '1.2.3.4',
+ request_method: 'GET',
+ fullpath: '/api/v4/internal/authorized_keys',
+ env: {
+ 'rack.attack.match_type' => match_type,
+ 'rack.attack.matched' => 'throttle_unauthenticated'
+ }
+ )
+ )
+ end
+
+ it 'logs request information' do
+ expect(Gitlab::AuthLogger).to receive(:error).with(
+ include(
+ message: 'Rack_Attack',
+ env: match_type,
+ remote_ip: '1.2.3.4',
+ request_method: 'GET',
+ path: '/api/v4/internal/authorized_keys',
+ matched: 'throttle_unauthenticated'
+ )
+ )
+ subscriber.send(match_type, event)
+ end
+ end
+
+ context 'when matched throttle requires user information' do
+ context 'when user not found' do
+ let(:event) do
+ ActiveSupport::Notifications::Event.new(
+ event_name, Time.current, Time.current + 2.seconds, '1', request: double(
+ :request,
+ ip: '1.2.3.4',
+ request_method: 'GET',
+ fullpath: '/api/v4/internal/authorized_keys',
+ env: {
+ 'rack.attack.match_type' => match_type,
+ 'rack.attack.matched' => 'throttle_authenticated_api',
+ 'rack.attack.match_discriminator' => 'not_exist_user_id'
+ }
+ )
+ )
+ end
+
+ it 'logs request information and user id' do
+ expect(Gitlab::AuthLogger).to receive(:error).with(
+ include(
+ message: 'Rack_Attack',
+ env: match_type,
+ remote_ip: '1.2.3.4',
+ request_method: 'GET',
+ path: '/api/v4/internal/authorized_keys',
+ matched: 'throttle_authenticated_api',
+ user_id: 'not_exist_user_id'
+ )
+ )
+ subscriber.send(match_type, event)
+ end
+ end
+
+ context 'when user found' do
+ let(:user) { create(:user) }
+ let(:event) do
+ ActiveSupport::Notifications::Event.new(
+ event_name, Time.current, Time.current + 2.seconds, '1', request: double(
+ :request,
+ ip: '1.2.3.4',
+ request_method: 'GET',
+ fullpath: '/api/v4/internal/authorized_keys',
+ env: {
+ 'rack.attack.match_type' => match_type,
+ 'rack.attack.matched' => 'throttle_authenticated_api',
+ 'rack.attack.match_discriminator' => user.id
+ }
+ )
+ )
+ end
+
+ it 'logs request information and user meta' do
+ expect(Gitlab::AuthLogger).to receive(:error).with(
+ include(
+ message: 'Rack_Attack',
+ env: match_type,
+ remote_ip: '1.2.3.4',
+ request_method: 'GET',
+ path: '/api/v4/internal/authorized_keys',
+ matched: 'throttle_authenticated_api',
+ user_id: user.id,
+ 'meta.user' => user.username
+ )
+ )
+ subscriber.send(match_type, event)
+ end
+ end
+ end
+ end
+
+ describe '#throttle' do
+ let(:match_type) { :throttle }
+ let(:event_name) { 'throttle.rack_attack' }
+
+ it_behaves_like 'log into auth logger'
+ end
+
+ describe '#blocklist' do
+ let(:match_type) { :blocklist }
+ let(:event_name) { 'blocklist.rack_attack' }
+
+ it_behaves_like 'log into auth logger'
+ end
+
+ describe '#track' do
+ let(:match_type) { :track }
+ let(:event_name) { 'track.rack_attack' }
+
+ it_behaves_like 'log into auth logger'
+ end
+
+ describe '#safelist' do
+ let(:event) do
+ ActiveSupport::Notifications::Event.new(
+ 'safelist.rack_attack', Time.current, Time.current + 2.seconds, '1', request: double(
+ :request,
+ env: {
+ 'rack.attack.matched' => 'throttle_unauthenticated'
+ }
+ )
+ )
+ end
+
+ it 'adds the matched name to safe request store' do
+ subscriber.safelist(event)
+ expect(Gitlab::SafeRequestStore[:instrumentation_throttle_safelist]).to eql('throttle_unauthenticated')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/middleware/request_context_spec.rb b/spec/lib/gitlab/middleware/request_context_spec.rb
index 431f4453e37..6d5b581feaa 100644
--- a/spec/lib/gitlab/middleware/request_context_spec.rb
+++ b/spec/lib/gitlab/middleware/request_context_spec.rb
@@ -18,9 +18,11 @@ RSpec.describe Gitlab::Middleware::RequestContext do
end
describe '#call' do
- context 'setting the client ip' do
- subject { Gitlab::RequestContext.instance.client_ip }
+ let(:instance) { Gitlab::RequestContext.instance }
+
+ subject { described_class.new(app).call(env) }
+ context 'setting the client ip' do
context 'with X-Forwarded-For headers' do
let(:load_balancer_ip) { '1.2.3.4' }
let(:headers) do
@@ -33,13 +35,7 @@ RSpec.describe Gitlab::Middleware::RequestContext do
let(:env) { Rack::MockRequest.env_for("/").merge(headers) }
it 'returns the load balancer IP' do
- endpoint = proc do
- [200, {}, ["Hello"]]
- end
-
- described_class.new(endpoint).call(env)
-
- expect(subject).to eq(load_balancer_ip)
+ expect { subject }.to change { instance.client_ip }.from(nil).to(load_balancer_ip)
end
end
@@ -47,32 +43,19 @@ RSpec.describe Gitlab::Middleware::RequestContext do
let(:ip) { '192.168.1.11' }
before do
- allow_next_instance_of(Rack::Request) do |instance|
- allow(instance).to receive(:ip).and_return(ip)
+ allow_next_instance_of(Rack::Request) do |request|
+ allow(request).to receive(:ip).and_return(ip)
end
- described_class.new(app).call(env)
end
- it { is_expected.to eq(ip) }
- end
+ it 'sets the `client_ip`' do
+ expect { subject }.to change { instance.client_ip }.from(nil).to(ip)
+ end
- context 'before RequestContext middleware run' do
- it { is_expected.to be_nil }
+ it 'sets the `request_start_time`' do
+ expect { subject }.to change { instance.request_start_time }.from(nil).to(Float)
+ end
end
end
end
-
- context 'setting the thread cpu time' do
- it 'sets the `start_thread_cpu_time`' do
- expect { described_class.new(app).call(env) }
- .to change { Gitlab::RequestContext.instance.start_thread_cpu_time }.from(nil).to(Float)
- end
- end
-
- context 'setting the request start time' do
- it 'sets the `request_start_time`' do
- expect { described_class.new(app).call(env) }
- .to change { Gitlab::RequestContext.instance.request_start_time }.from(nil).to(Float)
- end
- end
end
diff --git a/spec/lib/gitlab/pages_transfer_spec.rb b/spec/lib/gitlab/pages_transfer_spec.rb
index 4f0ee76b244..552a2e0701c 100644
--- a/spec/lib/gitlab/pages_transfer_spec.rb
+++ b/spec/lib/gitlab/pages_transfer_spec.rb
@@ -8,13 +8,24 @@ RSpec.describe Gitlab::PagesTransfer do
context 'when receiving an allowed method' do
it 'schedules a PagesTransferWorker', :aggregate_failures do
- described_class::Async::METHODS.each do |meth|
+ described_class::METHODS.each do |meth|
expect(PagesTransferWorker)
.to receive(:perform_async).with(meth, %w[foo bar])
async.public_send(meth, 'foo', 'bar')
end
end
+
+ it 'does nothing if legacy storage is disabled' do
+ stub_feature_flags(pages_update_legacy_storage: false)
+
+ described_class::METHODS.each do |meth|
+ expect(PagesTransferWorker)
+ .not_to receive(:perform_async)
+
+ async.public_send(meth, 'foo', 'bar')
+ end
+ end
end
context 'when receiving a private method' do
@@ -59,6 +70,15 @@ RSpec.describe Gitlab::PagesTransfer do
expect(subject.public_send(meth, *args)).to be(false)
end
+
+ it 'does nothing if legacy storage is disabled' do
+ stub_feature_flags(pages_update_legacy_storage: false)
+
+ subject.public_send(meth, *args)
+
+ expect(File.exist?(config_path_before)).to be(true)
+ expect(File.exist?(config_path_after)).to be(false)
+ end
end
describe '#move_namespace' do
diff --git a/spec/lib/gitlab/patch/prependable_spec.rb b/spec/lib/gitlab/patch/prependable_spec.rb
index 8feab57a8f3..5b01bb99fc8 100644
--- a/spec/lib/gitlab/patch/prependable_spec.rb
+++ b/spec/lib/gitlab/patch/prependable_spec.rb
@@ -231,4 +231,22 @@ RSpec.describe Gitlab::Patch::Prependable do
.to raise_error(described_class::MultiplePrependedBlocks)
end
end
+
+ describe 'the extra hack for override verification' do
+ context 'when ENV["STATIC_VERIFICATION"] is not defined' do
+ it 'does not extend ClassMethods onto the defining module' do
+ expect(ee).not_to respond_to(:class_name)
+ end
+ end
+
+ context 'when ENV["STATIC_VERIFICATION"] is defined' do
+ before do
+ stub_env('STATIC_VERIFICATION', 'true')
+ end
+
+ it 'does extend ClassMethods onto the defining module' do
+ expect(ee).to respond_to(:class_name)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/performance_bar/stats_spec.rb b/spec/lib/gitlab/performance_bar/stats_spec.rb
index c34c6f7b31f..ad11eca56d1 100644
--- a/spec/lib/gitlab/performance_bar/stats_spec.rb
+++ b/spec/lib/gitlab/performance_bar/stats_spec.rb
@@ -22,10 +22,12 @@ RSpec.describe Gitlab::PerformanceBar::Stats do
expect(logger).to receive(:info)
.with({ duration_ms: 1.096, filename: 'lib/gitlab/pagination/offset_pagination.rb',
- filenum: 53, method: 'add_pagination_headers', request_id: 'foo', type: :sql })
+ method_path: 'lib/gitlab/pagination/offset_pagination.rb:add_pagination_headers',
+ count: 1, request_id: 'foo', type: :sql })
expect(logger).to receive(:info)
- .with({ duration_ms: 0.817, filename: 'lib/api/helpers.rb',
- filenum: 112, method: 'find_project', request_id: 'foo', type: :sql }).twice
+ .with({ duration_ms: 1.634, filename: 'lib/api/helpers.rb',
+ method_path: 'lib/api/helpers.rb:find_project',
+ count: 2, request_id: 'foo', type: :sql })
subject
end
diff --git a/spec/lib/gitlab/rack_attack/instrumented_cache_store_spec.rb b/spec/lib/gitlab/rack_attack/instrumented_cache_store_spec.rb
new file mode 100644
index 00000000000..2cb31b00f39
--- /dev/null
+++ b/spec/lib/gitlab/rack_attack/instrumented_cache_store_spec.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::RackAttack::InstrumentedCacheStore do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:store) { ::ActiveSupport::Cache::NullStore.new }
+
+ subject { described_class.new(upstream_store: store)}
+
+ where(:operation, :params, :test_proc) do
+ :fetch | [:key] | ->(s) { s.fetch(:key) }
+ :read | [:key] | ->(s) { s.read(:key) }
+ :read_multi | [:key_1, :key_2, :key_3] | ->(s) { s.read_multi(:key_1, :key_2, :key_3) }
+ :write_multi | [{ key_1: 1, key_2: 2, key_3: 3 }] | ->(s) { s.write_multi(key_1: 1, key_2: 2, key_3: 3) }
+ :fetch_multi | [:key_1, :key_2, :key_3] | ->(s) { s.fetch_multi(:key_1, :key_2, :key_3) {} }
+ :write | [:key, :value, { option_1: 1 }] | ->(s) { s.write(:key, :value, option_1: 1) }
+ :delete | [:key] | ->(s) { s.delete(:key) }
+ :exist? | [:key, { option_1: 1 }] | ->(s) { s.exist?(:key, option_1: 1) }
+ :delete_matched | [/^key$/, { option_1: 1 }] | ->(s) { s.delete_matched(/^key$/, option_1: 1 ) }
+ :increment | [:key, 1] | ->(s) { s.increment(:key, 1) }
+ :decrement | [:key, 1] | ->(s) { s.decrement(:key, 1) }
+ :cleanup | [] | ->(s) { s.cleanup }
+ :clear | [] | ->(s) { s.clear }
+ end
+
+ with_them do
+ it 'publishes a notification' do
+ event = nil
+
+ begin
+ subscriber = ActiveSupport::Notifications.subscribe("redis.rack_attack") do |*args|
+ event = ActiveSupport::Notifications::Event.new(*args)
+ end
+
+ test_proc.call(subject)
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
+ end
+
+ expect(event).not_to be_nil
+ expect(event.name).to eq("redis.rack_attack")
+ expect(event.duration).to be_a(Float).and(be > 0.0)
+ expect(event.payload[:operation]).to eql(operation)
+ end
+
+ it 'publishes a notification even if the cache store returns an error' do
+ allow(store).to receive(operation).and_raise('Something went wrong')
+
+ event = nil
+ exception = nil
+
+ begin
+ subscriber = ActiveSupport::Notifications.subscribe("redis.rack_attack") do |*args|
+ event = ActiveSupport::Notifications::Event.new(*args)
+ end
+
+ begin
+ test_proc.call(subject)
+ rescue => e
+ exception = e
+ end
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
+ end
+
+ expect(event).not_to be_nil
+ expect(event.name).to eq("redis.rack_attack")
+ expect(event.duration).to be_a(Float).and(be > 0.0)
+ expect(event.payload[:operation]).to eql(operation)
+
+ expect(exception).not_to be_nil
+ expect(exception.message).to eql('Something went wrong')
+ end
+
+ it 'delegates to the upstream store' do
+ allow(store).to receive(operation).and_call_original
+
+ if params.empty?
+ expect(store).to receive(operation).with(no_args)
+ else
+ expect(store).to receive(operation).with(*params)
+ end
+
+ test_proc.call(subject)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/rack_attack_spec.rb b/spec/lib/gitlab/rack_attack_spec.rb
index 5748e1e49e5..788d2eac61f 100644
--- a/spec/lib/gitlab/rack_attack_spec.rb
+++ b/spec/lib/gitlab/rack_attack_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe Gitlab::RackAttack, :aggregate_failures do
describe '.configure' do
let(:fake_rack_attack) { class_double("Rack::Attack") }
let(:fake_rack_attack_request) { class_double("Rack::Attack::Request") }
+ let(:fake_cache) { instance_double("Rack::Attack::Cache") }
let(:throttles) do
{
@@ -27,6 +28,8 @@ RSpec.describe Gitlab::RackAttack, :aggregate_failures do
allow(fake_rack_attack).to receive(:track)
allow(fake_rack_attack).to receive(:safelist)
allow(fake_rack_attack).to receive(:blocklist)
+ allow(fake_rack_attack).to receive(:cache).and_return(fake_cache)
+ allow(fake_cache).to receive(:store=)
end
it 'extends the request class' do
diff --git a/spec/lib/gitlab/repository_cache_adapter_spec.rb b/spec/lib/gitlab/repository_cache_adapter_spec.rb
index 4c57665b41f..625dcf11546 100644
--- a/spec/lib/gitlab/repository_cache_adapter_spec.rb
+++ b/spec/lib/gitlab/repository_cache_adapter_spec.rb
@@ -292,12 +292,11 @@ RSpec.describe Gitlab::RepositoryCacheAdapter do
describe '#expire_method_caches' do
it 'expires the caches of the given methods' do
- expect(cache).to receive(:expire).with(:rendered_readme)
expect(cache).to receive(:expire).with(:branch_names)
- expect(redis_set_cache).to receive(:expire).with(:rendered_readme, :branch_names)
- expect(redis_hash_cache).to receive(:delete).with(:rendered_readme, :branch_names)
+ expect(redis_set_cache).to receive(:expire).with(:branch_names)
+ expect(redis_hash_cache).to receive(:delete).with(:branch_names)
- repository.expire_method_caches(%i(rendered_readme branch_names))
+ repository.expire_method_caches(%i(branch_names))
end
it 'does not expire caches for non-existent methods' do
diff --git a/spec/lib/gitlab/request_forgery_protection_spec.rb b/spec/lib/gitlab/request_forgery_protection_spec.rb
index 20996dd44b8..a7b777cf4f2 100644
--- a/spec/lib/gitlab/request_forgery_protection_spec.rb
+++ b/spec/lib/gitlab/request_forgery_protection_spec.rb
@@ -52,6 +52,11 @@ RSpec.describe Gitlab::RequestForgeryProtection, :allow_forgery_protection do
end
describe '.verified?' do
+ it 'does not modify the env' do
+ env['REQUEST_METHOD'] = "GET"
+ expect { described_class.verified?(env) }.not_to change { env }
+ end
+
context 'when the request method is GET' do
before do
env['REQUEST_METHOD'] = 'GET'
diff --git a/spec/lib/gitlab/runtime_spec.rb b/spec/lib/gitlab/runtime_spec.rb
index 8ed7cc141cd..1ec14092c63 100644
--- a/spec/lib/gitlab/runtime_spec.rb
+++ b/spec/lib/gitlab/runtime_spec.rb
@@ -44,10 +44,11 @@ RSpec.describe Gitlab::Runtime do
context "puma" do
let(:puma_type) { double('::Puma') }
+ let(:max_workers) { 2 }
before do
stub_const('::Puma', puma_type)
- allow(puma_type).to receive_message_chain(:cli_config, :options).and_return(max_threads: 2)
+ allow(puma_type).to receive_message_chain(:cli_config, :options).and_return(max_threads: 2, workers: max_workers)
stub_env('ACTION_CABLE_IN_APP', 'false')
end
@@ -70,6 +71,20 @@ RSpec.describe Gitlab::Runtime do
it_behaves_like "valid runtime", :puma, 11
end
+
+ describe ".puma_in_clustered_mode?" do
+ context 'when Puma is set up with workers > 0' do
+ let(:max_workers) { 4 }
+
+ specify { expect(described_class.puma_in_clustered_mode?).to be true }
+ end
+
+ context 'when Puma is set up with workers = 0' do
+ let(:max_workers) { 0 }
+
+ specify { expect(described_class.puma_in_clustered_mode?).to be false }
+ end
+ end
end
context "unicorn" do
diff --git a/spec/lib/gitlab/search/query_spec.rb b/spec/lib/gitlab/search/query_spec.rb
index dd2f23a7e47..234b683ba1f 100644
--- a/spec/lib/gitlab/search/query_spec.rb
+++ b/spec/lib/gitlab/search/query_spec.rb
@@ -46,4 +46,22 @@ RSpec.describe Gitlab::Search::Query do
expect(subject.filters).to all(include(negated: true))
end
end
+
+ context 'with filter value in quotes' do
+ let(:query) { '"foo bar" name:"my test script.txt"' }
+
+ it 'does not break the filter value in quotes' do
+ expect(subject.term).to eq('"foo bar"')
+ expect(subject.filters[0]).to include(name: :name, negated: false, value: "MY TEST SCRIPT.TXT")
+ end
+ end
+
+ context 'with extra white spaces between the query words' do
+ let(:query) { ' foo = bar name:"my test.txt"' }
+
+ it 'removes the extra whitespace between tokens' do
+ expect(subject.term).to eq('foo = bar')
+ expect(subject.filters[0]).to include(name: :name, negated: false, value: "MY TEST.TXT")
+ end
+ end
end
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index c437b6bcceb..a1b18172a31 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe Gitlab::SearchResults do
include ProjectForksHelper
include SearchHelpers
+ using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, name: 'foo') }
@@ -41,8 +42,6 @@ RSpec.describe Gitlab::SearchResults do
end
describe '#formatted_count' do
- using RSpec::Parameterized::TableSyntax
-
where(:scope, :count_method, :expected) do
'projects' | :limited_projects_count | max_limited_count
'issues' | :limited_issues_count | max_limited_count
@@ -61,8 +60,6 @@ RSpec.describe Gitlab::SearchResults do
end
describe '#highlight_map' do
- using RSpec::Parameterized::TableSyntax
-
where(:scope, :expected) do
'projects' | {}
'issues' | {}
@@ -80,8 +77,6 @@ RSpec.describe Gitlab::SearchResults do
end
describe '#formatted_limited_count' do
- using RSpec::Parameterized::TableSyntax
-
where(:count, :expected) do
23 | '23'
99 | '99'
@@ -183,12 +178,18 @@ RSpec.describe Gitlab::SearchResults do
end
context 'ordering' do
- let(:query) { 'sorted' }
let!(:old_result) { create(:merge_request, :opened, source_project: project, source_branch: 'old-1', title: 'sorted old', created_at: 1.month.ago) }
let!(:new_result) { create(:merge_request, :opened, source_project: project, source_branch: 'new-1', title: 'sorted recent', created_at: 1.day.ago) }
let!(:very_old_result) { create(:merge_request, :opened, source_project: project, source_branch: 'very-old-1', title: 'sorted very old', created_at: 1.year.ago) }
- include_examples 'search results sorted'
+ let!(:old_updated) { create(:merge_request, :opened, source_project: project, source_branch: 'updated-old-1', title: 'updated old', updated_at: 1.month.ago) }
+ let!(:new_updated) { create(:merge_request, :opened, source_project: project, source_branch: 'updated-new-1', title: 'updated recent', updated_at: 1.day.ago) }
+ let!(:very_old_updated) { create(:merge_request, :opened, source_project: project, source_branch: 'updated-very-old-1', title: 'updated very old', updated_at: 1.year.ago) }
+
+ include_examples 'search results sorted' do
+ let(:results_created) { described_class.new(user, 'sorted', Project.order(:id), sort: sort, filters: filters) }
+ let(:results_updated) { described_class.new(user, 'updated', Project.order(:id), sort: sort, filters: filters) }
+ end
end
end
@@ -219,12 +220,18 @@ RSpec.describe Gitlab::SearchResults do
end
context 'ordering' do
- let(:query) { 'sorted' }
let!(:old_result) { create(:issue, project: project, title: 'sorted old', created_at: 1.month.ago) }
let!(:new_result) { create(:issue, project: project, title: 'sorted recent', created_at: 1.day.ago) }
let!(:very_old_result) { create(:issue, project: project, title: 'sorted very old', created_at: 1.year.ago) }
- include_examples 'search results sorted'
+ let!(:old_updated) { create(:issue, project: project, title: 'updated old', updated_at: 1.month.ago) }
+ let!(:new_updated) { create(:issue, project: project, title: 'updated recent', updated_at: 1.day.ago) }
+ let!(:very_old_updated) { create(:issue, project: project, title: 'updated very old', updated_at: 1.year.ago) }
+
+ include_examples 'search results sorted' do
+ let(:results_created) { described_class.new(user, 'sorted', Project.order(:id), sort: sort, filters: filters) }
+ let(:results_updated) { described_class.new(user, 'updated', Project.order(:id), sort: sort, filters: filters) }
+ end
end
end
diff --git a/spec/lib/gitlab/sidekiq_logging/exception_handler_spec.rb b/spec/lib/gitlab/sidekiq_logging/exception_handler_spec.rb
deleted file mode 100644
index 94dcf6f9b9a..00000000000
--- a/spec/lib/gitlab/sidekiq_logging/exception_handler_spec.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::SidekiqLogging::ExceptionHandler do
- describe '#call' do
- let(:job) do
- {
- "class" => "TestWorker",
- "args" => [1234, 'hello'],
- "retry" => false,
- "queue" => "cronjob:test_queue",
- "queue_namespace" => "cronjob",
- "jid" => "da883554ee4fe414012f5f42",
- "correlation_id" => 'cid'
- }
- end
-
- let(:exception_message) { 'An error was thrown' }
- let(:backtrace) { caller }
- let(:exception) { RuntimeError.new(exception_message) }
- let(:logger) { double }
-
- before do
- allow(Sidekiq).to receive(:logger).and_return(logger)
- allow(exception).to receive(:backtrace).and_return(backtrace)
- end
-
- subject { described_class.new.call(exception, { context: 'Test', job: job }) }
-
- it 'logs job data into root tree' do
- expected_data = job.merge(
- error_class: 'RuntimeError',
- error_message: exception_message,
- context: 'Test',
- error_backtrace: Rails.backtrace_cleaner.clean(backtrace)
- )
-
- expect(logger).to receive(:warn).with(expected_data)
-
- subject
- end
- end
-end
diff --git a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
index b99a5352717..3e8e117ec71 100644
--- a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
+++ b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
@@ -3,7 +3,13 @@
require 'spec_helper'
RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
- describe '#call' do
+ before do
+ # We disable a memory instrumentation feature
+ # as this requires a special patched Ruby
+ allow(Gitlab::Memory::Instrumentation).to receive(:available?) { false }
+ end
+
+ describe '#call', :request_store do
let(:timestamp) { Time.iso8601('2018-01-01T12:00:00.000Z') }
let(:created_at) { timestamp - 1.second }
let(:scheduling_latency_s) { 1.0 }
@@ -21,14 +27,13 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
"correlation_id" => 'cid',
"error_message" => "wrong number of arguments (2 for 3)",
"error_class" => "ArgumentError",
- "error_backtrace" => [],
- "db_count" => 1,
- "db_write_count" => 0,
- "db_cached_count" => 0
+ "error_backtrace" => []
}
end
let(:logger) { double }
+ let(:clock_realtime_start) { 0.222222299 }
+ let(:clock_realtime_end) { 1.333333799 }
let(:clock_thread_cputime_start) { 0.222222299 }
let(:clock_thread_cputime_end) { 1.333333799 }
let(:start_payload) do
@@ -38,7 +43,8 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
'pid' => Process.pid,
'created_at' => created_at.to_f,
'enqueued_at' => created_at.to_f,
- 'scheduling_latency_s' => scheduling_latency_s
+ 'scheduling_latency_s' => scheduling_latency_s,
+ 'job_size_bytes' => be > 0
)
end
@@ -49,7 +55,10 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
'duration_s' => 0.0,
'completed_at' => timestamp.to_f,
'cpu_s' => 1.111112,
- 'db_duration_s' => 0.0
+ 'db_duration_s' => 0.0,
+ 'db_cached_count' => 0,
+ 'db_count' => 0,
+ 'db_write_count' => 0
)
end
@@ -58,7 +67,8 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
'message' => 'TestWorker JID-da883554ee4fe414012f5f42: fail: 0.0 sec',
'job_status' => 'fail',
'error_class' => 'ArgumentError',
- 'error_message' => 'some exception'
+ 'error_message' => 'Something went wrong',
+ 'error_backtrace' => be_a(Array).and(be_present)
)
end
@@ -67,7 +77,10 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
allow(subject).to receive(:current_time).and_return(timestamp.to_f)
- allow(Process).to receive(:clock_gettime).with(Process::CLOCK_THREAD_CPUTIME_ID).and_return(clock_thread_cputime_start, clock_thread_cputime_end)
+ allow(Process).to receive(:clock_gettime).with(Process::CLOCK_REALTIME, :float_second)
+ .and_return(clock_realtime_start, clock_realtime_end)
+ allow(Process).to receive(:clock_gettime).with(Process::CLOCK_THREAD_CPUTIME_ID, :float_second)
+ .and_return(clock_thread_cputime_start, clock_thread_cputime_end)
end
subject { described_class.new }
@@ -84,25 +97,97 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
expect(subject).to receive(:log_job_start).and_call_original
expect(subject).to receive(:log_job_done).and_call_original
- subject.call(job, 'test_queue') { }
+ call_subject(job, 'test_queue') { }
+ end
+ end
+
+ it 'logs real job wrapped by active job worker' do
+ wrapped_job = job.merge(
+ "class" => "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper",
+ "wrapped" => "TestWorker"
+ )
+
+ Timecop.freeze(timestamp) do
+ expect(logger).to receive(:info).with(start_payload).ordered
+ expect(logger).to receive(:info).with(end_payload).ordered
+ expect(subject).to receive(:log_job_start).and_call_original
+ expect(subject).to receive(:log_job_done).and_call_original
+
+ call_subject(wrapped_job, 'test_queue') { }
end
end
it 'logs an exception in job' do
Timecop.freeze(timestamp) do
expect(logger).to receive(:info).with(start_payload)
- expect(logger).to receive(:warn).with(hash_including(exception_payload))
+ expect(logger).to receive(:warn).with(include(exception_payload))
expect(subject).to receive(:log_job_start).and_call_original
expect(subject).to receive(:log_job_done).and_call_original
expect do
- subject.call(job, 'test_queue') do
- raise ArgumentError, 'some exception'
+ call_subject(job, 'test_queue') do
+ raise ArgumentError, 'Something went wrong'
end
end.to raise_error(ArgumentError)
end
end
+ it 'logs the root cause of an Sidekiq::JobRetry::Skip exception in the job' do
+ Timecop.freeze(timestamp) do
+ expect(logger).to receive(:info).with(start_payload)
+ expect(logger).to receive(:warn).with(include(exception_payload))
+ expect(subject).to receive(:log_job_start).and_call_original
+ expect(subject).to receive(:log_job_done).and_call_original
+
+ expect do
+ call_subject(job, 'test_queue') do
+ raise ArgumentError, 'Something went wrong'
+ rescue
+ raise Sidekiq::JobRetry::Skip
+ end
+ end.to raise_error(Sidekiq::JobRetry::Skip)
+ end
+ end
+
+ it 'logs the root cause of an Sidekiq::JobRetry::Handled exception in the job' do
+ Timecop.freeze(timestamp) do
+ expect(logger).to receive(:info).with(start_payload)
+ expect(logger).to receive(:warn).with(include(exception_payload))
+ expect(subject).to receive(:log_job_start).and_call_original
+ expect(subject).to receive(:log_job_done).and_call_original
+
+ expect do
+ call_subject(job, 'test_queue') do
+ raise ArgumentError, 'Something went wrong'
+ rescue
+ raise Sidekiq::JobRetry::Handled
+ end
+ end.to raise_error(Sidekiq::JobRetry::Handled)
+ end
+ end
+
+ it 'keeps Sidekiq::JobRetry::Handled exception if the cause does not exist' do
+ Timecop.freeze(timestamp) do
+ expect(logger).to receive(:info).with(start_payload)
+ expect(logger).to receive(:warn).with(
+ include(
+ 'message' => 'TestWorker JID-da883554ee4fe414012f5f42: fail: 0.0 sec',
+ 'job_status' => 'fail',
+ 'error_class' => 'Sidekiq::JobRetry::Skip',
+ 'error_message' => 'Sidekiq::JobRetry::Skip'
+ )
+ )
+ expect(subject).to receive(:log_job_start).and_call_original
+ expect(subject).to receive(:log_job_done).and_call_original
+
+ expect do
+ call_subject(job, 'test_queue') do
+ raise Sidekiq::JobRetry::Skip
+ end
+ end.to raise_error(Sidekiq::JobRetry::Skip)
+ end
+ end
+
it 'does not modify the job' do
Timecop.freeze(timestamp) do
job_copy = job.deep_dup
@@ -111,11 +196,29 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
allow(subject).to receive(:log_job_start).and_call_original
allow(subject).to receive(:log_job_done).and_call_original
- subject.call(job, 'test_queue') do
+ call_subject(job, 'test_queue') do
expect(job).to eq(job_copy)
end
end
end
+
+ it 'does not modify the wrapped job' do
+ Timecop.freeze(timestamp) do
+ wrapped_job = job.merge(
+ "class" => "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper",
+ "wrapped" => "TestWorker"
+ )
+ job_copy = wrapped_job.deep_dup
+
+ allow(logger).to receive(:info)
+ allow(subject).to receive(:log_job_start).and_call_original
+ allow(subject).to receive(:log_job_done).and_call_original
+
+ call_subject(wrapped_job, 'test_queue') do
+ expect(wrapped_job).to eq(job_copy)
+ end
+ end
+ end
end
context 'with SIDEKIQ_LOG_ARGUMENTS disabled' do
@@ -130,7 +233,7 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
expect(subject).to receive(:log_job_start).and_call_original
expect(subject).to receive(:log_job_done).and_call_original
- subject.call(job, 'test_queue') { }
+ call_subject(job, 'test_queue') { }
end
end
@@ -143,7 +246,7 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
expect(subject).to receive(:log_job_start).and_call_original
expect(subject).to receive(:log_job_done).and_call_original
- subject.call(job.except("created_at", "enqueued_at"), 'test_queue') { }
+ call_subject(job.except("created_at", "enqueued_at"), 'test_queue') { }
end
end
end
@@ -159,7 +262,7 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
expect(subject).to receive(:log_job_start).and_call_original
expect(subject).to receive(:log_job_done).and_call_original
- subject.call(job, 'test_queue') { }
+ call_subject(job, 'test_queue') { }
end
end
end
@@ -177,7 +280,7 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
end
let(:expected_end_payload) do
- end_payload.merge(timing_data)
+ end_payload.merge(timing_data.stringify_keys)
end
it 'logs with Gitaly and Rugged timing data' do
@@ -185,7 +288,7 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
expect(logger).to receive(:info).with(start_payload).ordered
expect(logger).to receive(:info).with(expected_end_payload).ordered
- subject.call(job, 'test_queue') do
+ call_subject(job, 'test_queue') do
job.merge!(timing_data)
end
end
@@ -207,7 +310,7 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
let(:expected_end_payload_with_db) do
expected_end_payload.merge(
'db_duration_s' => a_value >= 0.1,
- 'db_count' => 1,
+ 'db_count' => a_value >= 1,
'db_cached_count' => 0,
'db_write_count' => 0
)
@@ -217,7 +320,9 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
expect(logger).to receive(:info).with(expected_start_payload).ordered
expect(logger).to receive(:info).with(expected_end_payload_with_db).ordered
- subject.call(job, 'test_queue') { ActiveRecord::Base.connection.execute('SELECT pg_sleep(0.1);') }
+ call_subject(job, 'test_queue') do
+ ActiveRecord::Base.connection.execute('SELECT pg_sleep(0.1);')
+ end
end
it 'prevents database time from leaking to the next job' do
@@ -226,8 +331,13 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
expect(logger).to receive(:info).with(expected_start_payload).ordered
expect(logger).to receive(:info).with(expected_end_payload).ordered
- subject.call(job, 'test_queue') { ActiveRecord::Base.connection.execute('SELECT pg_sleep(0.1);') }
- subject.call(job, 'test_queue') { }
+ call_subject(job.dup, 'test_queue') do
+ ActiveRecord::Base.connection.execute('SELECT pg_sleep(0.1);')
+ end
+
+ Gitlab::SafeRequestStore.clear!
+
+ call_subject(job.dup, 'test_queue') { }
end
end
@@ -243,7 +353,7 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
expect(logger).to receive(:info).with(expected_start_payload).ordered
expect(logger).to receive(:info).with(expected_end_payload).ordered
- subject.call(job, 'test_queue') do
+ call_subject(job, 'test_queue') do
job["#{ApplicationWorker::LOGGING_EXTRA_KEY}.key1"] = 15
job["#{ApplicationWorker::LOGGING_EXTRA_KEY}.key2"] = 16
job['key that will be ignored because it does not start with extra.'] = 17
@@ -251,13 +361,29 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
end
end
end
+
+ def call_subject(job, queue)
+ # This structured logger strongly depends on execution of `InstrumentationLogger`
+ subject.call(job, queue) do
+ ::Gitlab::SidekiqMiddleware::InstrumentationLogger.new.call('worker', job, queue) do
+ yield
+ end
+ end
+ end
end
describe '#add_time_keys!' do
- let(:time) { { duration: 0.1231234, cputime: 1.2342345 } }
+ let(:time) { { duration: 0.1231234 } }
let(:payload) { { 'class' => 'my-class', 'message' => 'my-message', 'job_status' => 'my-job-status' } }
let(:current_utc_time) { Time.now.utc }
- let(:payload_with_time_keys) { { 'class' => 'my-class', 'message' => 'my-message', 'job_status' => 'my-job-status', 'duration_s' => 0.123123, 'cpu_s' => 1.234235, 'completed_at' => current_utc_time.to_f } }
+
+ let(:payload_with_time_keys) do
+ { 'class' => 'my-class',
+ 'message' => 'my-message',
+ 'job_status' => 'my-job-status',
+ 'duration_s' => 0.123123,
+ 'completed_at' => current_utc_time.to_f }
+ end
subject { described_class.new }
diff --git a/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb
index f7010b2001a..e2b36125b4e 100644
--- a/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb
@@ -60,6 +60,27 @@ RSpec.describe Gitlab::SidekiqMiddleware::ClientMetrics do
end
end
+ context "when a worker is wrapped into ActiveJob" do
+ before do
+ stub_const('TestWrappedWorker', Class.new)
+ TestWrappedWorker.class_eval do
+ include Sidekiq::Worker
+ end
+ end
+
+ it_behaves_like "a metrics client middleware" do
+ let(:job) do
+ {
+ "class" => ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper,
+ "wrapped" => TestWrappedWorker
+ }
+ end
+
+ let(:worker) { TestWrappedWorker.new }
+ let(:labels) { default_labels.merge(urgency: "") }
+ end
+ end
+
context "when workers are attributed" do
def create_attributed_worker_class(urgency, external_dependencies, resource_boundary, category)
klass = Class.new do
diff --git a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
index 44bfaf4cc3c..e58e41d3e4f 100644
--- a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
@@ -198,6 +198,28 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do
it_behaves_like "a metrics middleware"
end
+ context "when a worker is wrapped into ActiveJob" do
+ before do
+ stub_const('TestWrappedWorker', Class.new)
+ TestWrappedWorker.class_eval do
+ include Sidekiq::Worker
+ end
+ end
+
+ let(:job) do
+ {
+ "class" => ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper,
+ "wrapped" => TestWrappedWorker
+ }
+ end
+
+ let(:worker) { TestWrappedWorker.new }
+ let(:worker_class) { TestWrappedWorker }
+ let(:labels) { default_labels.merge(urgency: "") }
+
+ it_behaves_like "a metrics middleware"
+ end
+
context "when workers are attributed" do
def create_attributed_worker_class(urgency, external_dependencies, resource_boundary, category)
Class.new do
diff --git a/spec/lib/gitlab/suggestions/commit_message_spec.rb b/spec/lib/gitlab/suggestions/commit_message_spec.rb
index 1411f64f8b7..965960f0c3e 100644
--- a/spec/lib/gitlab/suggestions/commit_message_spec.rb
+++ b/spec/lib/gitlab/suggestions/commit_message_spec.rb
@@ -72,6 +72,17 @@ RSpec.describe Gitlab::Suggestions::CommitMessage do
end
end
+ context 'when a custom commit message is specified' do
+ let(:message) { "i'm a project message. a user's custom message takes precedence over me :(" }
+ let(:custom_message) { "hello there! i'm a cool custom commit message." }
+
+ it 'shows the custom commit message' do
+ expect(Gitlab::Suggestions::CommitMessage
+ .new(user, suggestion_set, custom_message)
+ .message).to eq(custom_message)
+ end
+ end
+
context 'is specified and includes all placeholders' do
let(:message) do
'*** %{branch_name} %{files_count} %{file_paths} %{project_name} %{project_path} %{user_full_name} %{username} %{suggestions_count} ***'
diff --git a/spec/lib/gitlab/template/finders/global_template_finder_spec.rb b/spec/lib/gitlab/template/finders/global_template_finder_spec.rb
index e2751d194d3..38ec28c2b9a 100644
--- a/spec/lib/gitlab/template/finders/global_template_finder_spec.rb
+++ b/spec/lib/gitlab/template/finders/global_template_finder_spec.rb
@@ -15,9 +15,19 @@ RSpec.describe Gitlab::Template::Finders::GlobalTemplateFinder do
FileUtils.rm_rf(base_dir)
end
- subject(:finder) { described_class.new(base_dir, '', { 'General' => '', 'Bar' => 'Bar' }, excluded_patterns: excluded_patterns) }
+ subject(:finder) do
+ described_class.new(base_dir, '',
+ { 'General' => '', 'Bar' => 'Bar' },
+ include_categories_for_file,
+ excluded_patterns: excluded_patterns)
+ end
let(:excluded_patterns) { [] }
+ let(:include_categories_for_file) do
+ {
+ "SAST" => { "Security" => "Security" }
+ }
+ end
describe '.find' do
context 'with a non-prefixed General template' do
@@ -60,6 +70,7 @@ RSpec.describe Gitlab::Template::Finders::GlobalTemplateFinder do
context 'with a prefixed template' do
before do
create_template!('Bar/test-template')
+ create_template!('Security/SAST')
end
it 'finds the template with a prefix' do
@@ -76,6 +87,16 @@ RSpec.describe Gitlab::Template::Finders::GlobalTemplateFinder do
expect { finder.find('../foo') }.to raise_error(/Invalid path/)
end
+ context 'with include_categories_for_file being present' do
+ it 'finds the template with a prefix' do
+ expect(finder.find('SAST')).to be_present
+ end
+
+ it 'does not find any template which is missing in include_categories_for_file' do
+ expect(finder.find('DAST')).to be_nil
+ end
+ end
+
context 'while listed as an exclusion' do
let(:excluded_patterns) { [%r{^Bar/test-template$}] }
diff --git a/spec/lib/gitlab/terraform/state_migration_helper_spec.rb b/spec/lib/gitlab/terraform/state_migration_helper_spec.rb
new file mode 100644
index 00000000000..36c9c060e98
--- /dev/null
+++ b/spec/lib/gitlab/terraform/state_migration_helper_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Terraform::StateMigrationHelper do
+ before do
+ stub_terraform_state_object_storage
+ end
+
+ describe '.migrate_to_remote_storage' do
+ let!(:local_version) { create(:terraform_state_version, file_store: Terraform::StateUploader::Store::LOCAL) }
+
+ subject { described_class.migrate_to_remote_storage }
+
+ it 'migrates remote files to remote storage' do
+ subject
+
+ expect(local_version.reload.file_store).to eq(Terraform::StateUploader::Store::REMOTE)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/tracking/standard_context_spec.rb b/spec/lib/gitlab/tracking/standard_context_spec.rb
index acf7aeb303a..7a0a4f0cc46 100644
--- a/spec/lib/gitlab/tracking/standard_context_spec.rb
+++ b/spec/lib/gitlab/tracking/standard_context_spec.rb
@@ -9,47 +9,48 @@ RSpec.describe Gitlab::Tracking::StandardContext do
let(:snowplow_context) { subject.to_context }
describe '#to_context' do
- context 'with no arguments' do
- it 'creates a Snowplow context with no data' do
- snowplow_context.to_json[:data].each do |_, v|
- expect(v).to be_nil
+ context 'environment' do
+ shared_examples 'contains environment' do |expected_environment|
+ it 'contains environment' do
+ expect(snowplow_context.to_json.dig(:data, :environment)).to eq(expected_environment)
end
end
- end
- context 'with extra data' do
- subject { described_class.new(foo: 'bar') }
-
- it 'creates a Snowplow context with the given data' do
- expect(snowplow_context.to_json.dig(:data, :foo)).to eq('bar')
+ context 'development or test' do
+ include_examples 'contains environment', 'development'
end
- end
- context 'with namespace' do
- subject { described_class.new(namespace: namespace) }
+ context 'staging' do
+ before do
+ allow(Gitlab).to receive(:staging?).and_return(true)
+ end
- it 'creates a Snowplow context using the given data' do
- expect(snowplow_context.to_json.dig(:data, :namespace_id)).to eq(namespace.id)
- expect(snowplow_context.to_json.dig(:data, :project_id)).to be_nil
+ include_examples 'contains environment', 'staging'
end
- end
- context 'with project' do
- subject { described_class.new(project: project) }
+ context 'production' do
+ before do
+ allow(Gitlab).to receive(:com_and_canary?).and_return(true)
+ end
- it 'creates a Snowplow context using the given data' do
- expect(snowplow_context.to_json.dig(:data, :namespace_id)).to eq(project.namespace.id)
- expect(snowplow_context.to_json.dig(:data, :project_id)).to eq(project.id)
+ include_examples 'contains environment', 'production'
end
end
- context 'with project and namespace' do
- subject { described_class.new(namespace: namespace, project: project) }
+ it 'contains source' do
+ expect(snowplow_context.to_json.dig(:data, :source)).to eq(described_class::GITLAB_RAILS_SOURCE)
+ end
+
+ context 'with extra data' do
+ subject { described_class.new(foo: 'bar') }
- it 'creates a Snowplow context using the given data' do
- expect(snowplow_context.to_json.dig(:data, :namespace_id)).to eq(namespace.id)
- expect(snowplow_context.to_json.dig(:data, :project_id)).to eq(project.id)
+ it 'creates a Snowplow context with the given data' do
+ expect(snowplow_context.to_json.dig(:data, :foo)).to eq('bar')
end
end
+
+ it 'does not contain any ids' do
+ expect(snowplow_context.to_json[:data].keys).not_to include(:user_id, :project_id, :namespace_id)
+ end
end
end
diff --git a/spec/lib/gitlab/tracking_spec.rb b/spec/lib/gitlab/tracking_spec.rb
index 8f1fd49f4c5..80740c8112e 100644
--- a/spec/lib/gitlab/tracking_spec.rb
+++ b/spec/lib/gitlab/tracking_spec.rb
@@ -42,36 +42,31 @@ RSpec.describe Gitlab::Tracking do
end
shared_examples 'delegates to destination' do |klass|
- context 'with standard context' do
- it "delegates to #{klass} destination" do
- expect_any_instance_of(klass).to receive(:event) do |_, category, action, args|
- expect(category).to eq('category')
- expect(action).to eq('action')
- expect(args[:label]).to eq('label')
- expect(args[:property]).to eq('property')
- expect(args[:value]).to eq(1.5)
- expect(args[:context].length).to eq(1)
- expect(args[:context].first.to_json[:schema]).to eq(Gitlab::Tracking::StandardContext::GITLAB_STANDARD_SCHEMA_URL)
- expect(args[:context].first.to_json[:data]).to include(foo: 'bar')
- end
+ it "delegates to #{klass} destination" do
+ other_context = double(:context)
- described_class.event('category', 'action', label: 'label', property: 'property', value: 1.5,
- standard_context: Gitlab::Tracking::StandardContext.new(foo: 'bar'))
- end
- end
+ project = double(:project)
+ user = double(:user)
+ namespace = double(:namespace)
- context 'without standard context' do
- it "delegates to #{klass} destination" do
- expect_any_instance_of(klass).to receive(:event) do |_, category, action, args|
- expect(category).to eq('category')
- expect(action).to eq('action')
- expect(args[:label]).to eq('label')
- expect(args[:property]).to eq('property')
- expect(args[:value]).to eq(1.5)
- end
+ expect(Gitlab::Tracking::StandardContext)
+ .to receive(:new)
+ .with(project: project, user: user, namespace: namespace)
+ .and_call_original
- described_class.event('category', 'action', label: 'label', property: 'property', value: 1.5)
+ expect_any_instance_of(klass).to receive(:event) do |_, category, action, args|
+ expect(category).to eq('category')
+ expect(action).to eq('action')
+ expect(args[:label]).to eq('label')
+ expect(args[:property]).to eq('property')
+ expect(args[:value]).to eq(1.5)
+ expect(args[:context].length).to eq(2)
+ expect(args[:context].first).to eq(other_context)
+ expect(args[:context].last.to_json[:schema]).to eq(Gitlab::Tracking::StandardContext::GITLAB_STANDARD_SCHEMA_URL)
end
+
+ described_class.event('category', 'action', label: 'label', property: 'property', value: 1.5,
+ context: [other_context], project: project, user: user, namespace: namespace)
end
end
diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb
index 686382dc262..fa01d4e48df 100644
--- a/spec/lib/gitlab/url_blocker_spec.rb
+++ b/spec/lib/gitlab/url_blocker_spec.rb
@@ -302,36 +302,36 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
it 'does not block urls from private networks' do
local_ips.each do |ip|
stub_domain_resolv(fake_domain, ip) do
- expect(described_class).not_to be_blocked_url("http://#{fake_domain}", url_blocker_attributes)
+ expect(described_class).not_to be_blocked_url("http://#{fake_domain}", **url_blocker_attributes)
end
- expect(described_class).not_to be_blocked_url("http://#{ip}", url_blocker_attributes)
+ expect(described_class).not_to be_blocked_url("http://#{ip}", **url_blocker_attributes)
end
end
it 'allows localhost endpoints' do
- expect(described_class).not_to be_blocked_url('http://0.0.0.0', url_blocker_attributes)
- expect(described_class).not_to be_blocked_url('http://localhost', url_blocker_attributes)
- expect(described_class).not_to be_blocked_url('http://127.0.0.1', url_blocker_attributes)
+ expect(described_class).not_to be_blocked_url('http://0.0.0.0', **url_blocker_attributes)
+ expect(described_class).not_to be_blocked_url('http://localhost', **url_blocker_attributes)
+ expect(described_class).not_to be_blocked_url('http://127.0.0.1', **url_blocker_attributes)
end
it 'allows loopback endpoints' do
- expect(described_class).not_to be_blocked_url('http://127.0.0.2', url_blocker_attributes)
+ expect(described_class).not_to be_blocked_url('http://127.0.0.2', **url_blocker_attributes)
end
it 'allows IPv4 link-local endpoints' do
- expect(described_class).not_to be_blocked_url('http://169.254.169.254', url_blocker_attributes)
- expect(described_class).not_to be_blocked_url('http://169.254.168.100', url_blocker_attributes)
+ expect(described_class).not_to be_blocked_url('http://169.254.169.254', **url_blocker_attributes)
+ expect(described_class).not_to be_blocked_url('http://169.254.168.100', **url_blocker_attributes)
end
it 'allows IPv6 link-local endpoints' do
- expect(described_class).not_to be_blocked_url('http://[0:0:0:0:0:ffff:169.254.169.254]', url_blocker_attributes)
- expect(described_class).not_to be_blocked_url('http://[::ffff:169.254.169.254]', url_blocker_attributes)
- expect(described_class).not_to be_blocked_url('http://[::ffff:a9fe:a9fe]', url_blocker_attributes)
- expect(described_class).not_to be_blocked_url('http://[0:0:0:0:0:ffff:169.254.168.100]', url_blocker_attributes)
- expect(described_class).not_to be_blocked_url('http://[::ffff:169.254.168.100]', url_blocker_attributes)
- expect(described_class).not_to be_blocked_url('http://[::ffff:a9fe:a864]', url_blocker_attributes)
- expect(described_class).not_to be_blocked_url('http://[fe80::c800:eff:fe74:8]', url_blocker_attributes)
+ expect(described_class).not_to be_blocked_url('http://[0:0:0:0:0:ffff:169.254.169.254]', **url_blocker_attributes)
+ expect(described_class).not_to be_blocked_url('http://[::ffff:169.254.169.254]', **url_blocker_attributes)
+ expect(described_class).not_to be_blocked_url('http://[::ffff:a9fe:a9fe]', **url_blocker_attributes)
+ expect(described_class).not_to be_blocked_url('http://[0:0:0:0:0:ffff:169.254.168.100]', **url_blocker_attributes)
+ expect(described_class).not_to be_blocked_url('http://[::ffff:169.254.168.100]', **url_blocker_attributes)
+ expect(described_class).not_to be_blocked_url('http://[::ffff:a9fe:a864]', **url_blocker_attributes)
+ expect(described_class).not_to be_blocked_url('http://[fe80::c800:eff:fe74:8]', **url_blocker_attributes)
end
end
@@ -416,11 +416,11 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
attrs = url_blocker_attributes.merge(dns_rebind_protection: false)
stub_domain_resolv('example.com', '192.168.1.2') do
- expect(described_class).not_to be_blocked_url(url, attrs)
+ expect(described_class).not_to be_blocked_url(url, **attrs)
end
stub_domain_resolv('example.com', '192.168.1.3') do
- expect(described_class).to be_blocked_url(url, attrs)
+ expect(described_class).to be_blocked_url(url, **attrs)
end
end
end
@@ -442,18 +442,18 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
stub_domain_resolv(domain, '192.168.1.1') do
expect(described_class).not_to be_blocked_url("http://#{domain}",
- url_blocker_attributes)
+ **url_blocker_attributes)
end
stub_domain_resolv(subdomain1, '192.168.1.1') do
expect(described_class).not_to be_blocked_url("http://#{subdomain1}",
- url_blocker_attributes)
+ **url_blocker_attributes)
end
# subdomain2 is not part of the allowlist so it should be blocked
stub_domain_resolv(subdomain2, '192.168.1.1') do
expect(described_class).to be_blocked_url("http://#{subdomain2}",
- url_blocker_attributes)
+ **url_blocker_attributes)
end
end
@@ -463,12 +463,12 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
stub_domain_resolv(unicode_domain, '192.168.1.1') do
expect(described_class).not_to be_blocked_url("http://#{unicode_domain}",
- url_blocker_attributes)
+ **url_blocker_attributes)
end
stub_domain_resolv(idna_encoded_domain, '192.168.1.1') do
expect(described_class).not_to be_blocked_url("http://#{idna_encoded_domain}",
- url_blocker_attributes)
+ **url_blocker_attributes)
end
end
@@ -525,7 +525,7 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
it 'allows domain with port when resolved ip has port allowed' do
stub_domain_resolv("www.resolve-domain.com", '127.0.0.1') do
- expect(described_class).not_to be_blocked_url("http://www.resolve-domain.com:2000", url_blocker_attributes)
+ expect(described_class).not_to be_blocked_url("http://www.resolve-domain.com:2000", **url_blocker_attributes)
end
end
end
diff --git a/spec/lib/gitlab/url_blockers/url_allowlist_spec.rb b/spec/lib/gitlab/url_blockers/url_allowlist_spec.rb
index d9e44e9b85c..4c4248b143e 100644
--- a/spec/lib/gitlab/url_blockers/url_allowlist_spec.rb
+++ b/spec/lib/gitlab/url_blockers/url_allowlist_spec.rb
@@ -37,19 +37,19 @@ RSpec.describe Gitlab::UrlBlockers::UrlAllowlist do
let(:allowlist) { ['example.io:3000'] }
it 'returns true if domain and ports present in allowlist' do
- parsed_allowlist = [['example.io', { port: 3000 }]]
+ parsed_allowlist = [['example.io', 3000]]
not_allowed = [
'example.io',
- ['example.io', { port: 3001 }]
+ ['example.io', 3001]
]
aggregate_failures do
- parsed_allowlist.each do |domain_and_port|
- expect(described_class).to be_domain_allowed(*domain_and_port)
+ parsed_allowlist.each do |domain, port|
+ expect(described_class).to be_domain_allowed(domain, port: port)
end
- not_allowed.each do |domain_and_port|
- expect(described_class).not_to be_domain_allowed(*domain_and_port)
+ not_allowed.each do |domain, port|
+ expect(described_class).not_to be_domain_allowed(domain, port: port)
end
end
end
@@ -139,23 +139,23 @@ RSpec.describe Gitlab::UrlBlockers::UrlAllowlist do
it 'returns true if ip and ports present in allowlist' do
parsed_allowlist = [
- ['127.0.0.9', { port: 3000 }],
- ['[2001:db8:85a3:8d3:1319:8a2e:370:7348]', { port: 443 }]
+ ['127.0.0.9', 3000],
+ ['[2001:db8:85a3:8d3:1319:8a2e:370:7348]', 443]
]
not_allowed = [
'127.0.0.9',
- ['127.0.0.9', { port: 3001 }],
+ ['127.0.0.9', 3001],
'[2001:db8:85a3:8d3:1319:8a2e:370:7348]',
- ['[2001:db8:85a3:8d3:1319:8a2e:370:7348]', { port: 3001 }]
+ ['[2001:db8:85a3:8d3:1319:8a2e:370:7348]', 3001]
]
aggregate_failures do
- parsed_allowlist.each do |ip_and_port|
- expect(described_class).to be_ip_allowed(*ip_and_port)
+ parsed_allowlist.each do |ip, port|
+ expect(described_class).to be_ip_allowed(ip, port: port)
end
- not_allowed.each do |ip_and_port|
- expect(described_class).not_to be_ip_allowed(*ip_and_port)
+ not_allowed.each do |ip, port|
+ expect(described_class).not_to be_ip_allowed(ip, port: port)
end
end
end
diff --git a/spec/lib/gitlab/usage/docs/renderer_spec.rb b/spec/lib/gitlab/usage/docs/renderer_spec.rb
new file mode 100644
index 00000000000..0677aa2d9d7
--- /dev/null
+++ b/spec/lib/gitlab/usage/docs/renderer_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Docs::Renderer do
+ describe 'contents' do
+ let(:dictionary_path) { Gitlab::Usage::Docs::Renderer::DICTIONARY_PATH }
+ let(:items) { Gitlab::Usage::MetricDefinition.definitions }
+
+ it 'generates dictionary for given items' do
+ generated_dictionary = described_class.new(items).contents
+ generated_dictionary_keys = RDoc::Markdown
+ .parse(generated_dictionary)
+ .table_of_contents
+ .select { |metric_doc| metric_doc.level == 2 && !metric_doc.text.start_with?('info:') }
+ .map(&:text)
+ .map { |text| text.sub('<code>', '').sub('</code>', '') }
+
+ expect(generated_dictionary_keys).to match_array(items.keys)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/usage/docs/value_formatter_spec.rb b/spec/lib/gitlab/usage/docs/value_formatter_spec.rb
new file mode 100644
index 00000000000..7002c76a7cf
--- /dev/null
+++ b/spec/lib/gitlab/usage/docs/value_formatter_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Docs::ValueFormatter do
+ describe '.format' do
+ using RSpec::Parameterized::TableSyntax
+ where(:key, :value, :expected_value) do
+ :product_group | 'growth::product intelligence' | '`growth::product intelligence`'
+ :data_source | 'redis' | 'Redis'
+ :data_source | 'ruby' | 'Ruby'
+ :introduced_by_url | 'http://test.com' | '[Introduced by](http://test.com)'
+ :tier | %w(gold premium) | 'gold, premium'
+ :distribution | %w(ce ee) | 'ce, ee'
+ :key_path | 'key.path' | '**`key.path`**'
+ :milestone | '13.4' | '13.4'
+ :status | 'data_available' | 'data_available'
+ end
+
+ with_them do
+ subject { described_class.format(key, value) }
+
+ it { is_expected.to eq(expected_value) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/usage/metric_definition_spec.rb b/spec/lib/gitlab/usage/metric_definition_spec.rb
index e101f837324..8b592838f5d 100644
--- a/spec/lib/gitlab/usage/metric_definition_spec.rb
+++ b/spec/lib/gitlab/usage/metric_definition_spec.rb
@@ -5,18 +5,14 @@ require 'spec_helper'
RSpec.describe Gitlab::Usage::MetricDefinition do
let(:attributes) do
{
- name: 'uuid',
description: 'GitLab instance unique identifier',
value_type: 'string',
product_category: 'collection',
- stage: 'growth',
+ product_stage: 'growth',
status: 'data_available',
default_generation: 'generation_1',
- full_path: {
- generation_1: 'uuid',
- generation_2: 'license.uuid'
- },
- group: 'group::product analytics',
+ key_path: 'uuid',
+ product_group: 'group::product analytics',
time_frame: 'none',
data_source: 'database',
distribution: %w(ee ce),
@@ -44,13 +40,12 @@ RSpec.describe Gitlab::Usage::MetricDefinition do
using RSpec::Parameterized::TableSyntax
where(:attribute, :value) do
- :name | nil
:description | nil
:value_type | nil
:value_type | 'test'
:status | nil
- :default_generation | nil
- :group | nil
+ :key_path | nil
+ :product_group | nil
:time_frame | nil
:time_frame | '29d'
:data_source | 'other'
@@ -70,6 +65,20 @@ RSpec.describe Gitlab::Usage::MetricDefinition do
described_class.new(path, attributes).validate!
end
+
+ context 'with skip_validation' do
+ it 'raise exception if skip_validation: false' do
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).at_least(:once).with(instance_of(Gitlab::Usage::Metric::InvalidMetricError))
+
+ described_class.new(path, attributes.merge( { skip_validation: false } )).validate!
+ end
+
+ it 'does not raise exception if has skip_validation: true' do
+ expect(Gitlab::ErrorTracking).not_to receive(:track_and_raise_for_dev_exception)
+
+ described_class.new(path, attributes.merge( { skip_validation: true } )).validate!
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/usage/metric_spec.rb b/spec/lib/gitlab/usage/metric_spec.rb
index 40671d980d6..d4a789419a4 100644
--- a/spec/lib/gitlab/usage/metric_spec.rb
+++ b/spec/lib/gitlab/usage/metric_spec.rb
@@ -4,15 +4,15 @@ require 'spec_helper'
RSpec.describe Gitlab::Usage::Metric do
describe '#definition' do
- it 'returns generation_1 metric definiton' do
- expect(described_class.new(default_generation_path: 'uuid').definition).to be_an(Gitlab::Usage::MetricDefinition)
+ it 'returns key_path metric definiton' do
+ expect(described_class.new(key_path: 'uuid').definition).to be_an(Gitlab::Usage::MetricDefinition)
end
end
describe '#unflatten_default_path' do
using RSpec::Parameterized::TableSyntax
- where(:default_generation_path, :value, :expected_hash) do
+ where(:key_path, :value, :expected_hash) do
'uuid' | nil | { uuid: nil }
'uuid' | '1111' | { uuid: '1111' }
'counts.issues' | nil | { counts: { issues: nil } }
@@ -21,7 +21,7 @@ RSpec.describe Gitlab::Usage::Metric do
end
with_them do
- subject { described_class.new(default_generation_path: default_generation_path, value: value).unflatten_default_path }
+ subject { described_class.new(key_path: key_path, value: value).unflatten_key_path }
it { is_expected.to eq(expected_hash) }
end
diff --git a/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb b/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb
new file mode 100644
index 00000000000..5469ded18f9
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb
@@ -0,0 +1,281 @@
+# frozen_string_literal: true
+
+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_it_be(:recorded_at) { Time.current.to_i }
+
+ 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
+ end
+
+ context 'there are aggregated metrics defined' do
+ before do
+ allow_next_instance_of(described_class) do |instance|
+ allow(instance).to receive(:aggregated_metrics).and_return(aggregated_metrics)
+ end
+ end
+
+ context 'with disabled database_sourced_aggregated_metrics feature flag' do
+ before do
+ stub_feature_flags(database_sourced_aggregated_metrics: false)
+ end
+
+ let(:aggregated_metrics) do
+ [
+ { name: 'gmau_1', source: 'redis', events: %w[event3 event5], operator: "OR" },
+ { name: 'gmau_2', source: 'database', events: %w[event1 event2 event3], operator: "OR" }
+ ].map(&:with_indifferent_access)
+ end
+
+ it 'skips database sourced metrics', :aggregate_failures do
+ results = {
+ 'gmau_1' => 5
+ }
+
+ params = { start_date: start_date, end_date: end_date, recorded_at: recorded_at }
+
+ expect(sources::RedisHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event3 event5])).and_return(5)
+ expect(sources::PostgresHll).not_to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event1 event2 event3]))
+ expect(aggregated_metrics_data).to eq(results)
+ end
+ end
+
+ context 'with AND operator' do
+ let(:aggregated_metrics) do
+ [
+ { name: 'gmau_1', source: 'redis', events: %w[event3 event5], operator: "AND" },
+ { name: 'gmau_2', source: 'database', events: %w[event1 event2 event3], operator: "AND" }
+ ].map(&:with_indifferent_access)
+ end
+
+ it 'returns the number of unique events recorded for every metric in aggregate', :aggregate_failures do
+ results = {
+ 'gmau_1' => 2,
+ 'gmau_2' => 1
+ }
+ params = { start_date: start_date, end_date: end_date, recorded_at: recorded_at }
+
+ # gmau_1 data is as follow
+ # |A| => 4
+ expect(sources::RedisHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: 'event3')).and_return(4)
+ # |B| => 6
+ expect(sources::RedisHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: 'event5')).and_return(6)
+ # |A + B| => 8
+ expect(sources::RedisHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event3 event5])).and_return(8)
+ # Exclusion inclusion principle formula to calculate intersection of 2 sets
+ # |A & B| = (|A| + |B|) - |A + B| => (4 + 6) - 8 => 2
+
+ # gmau_2 data is as follow:
+ # |A| => 2
+ expect(sources::PostgresHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: 'event1')).and_return(2)
+ # |B| => 3
+ expect(sources::PostgresHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: 'event2')).and_return(3)
+ # |C| => 5
+ expect(sources::PostgresHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: 'event3')).and_return(5)
+
+ # |A + B| => 4 therefore |A & B| = (|A| + |B|) - |A + B| => 2 + 3 - 4 => 1
+ expect(sources::PostgresHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event1 event2])).and_return(4)
+ # |A + C| => 6 therefore |A & C| = (|A| + |C|) - |A + C| => 2 + 5 - 6 => 1
+ expect(sources::PostgresHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event1 event3])).and_return(6)
+ # |B + C| => 7 therefore |B & C| = (|B| + |C|) - |B + C| => 3 + 5 - 7 => 1
+ expect(sources::PostgresHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event2 event3])).and_return(7)
+ # |A + B + C| => 8
+ expect(sources::PostgresHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event1 event2 event3])).and_return(8)
+ # Exclusion inclusion principle formula to calculate intersection of 3 sets
+ # |A & B & C| = (|A & B| + |A & C| + |B & C|) - (|A| + |B| + |C|) + |A + B + C|
+ # (1 + 1 + 1) - (2 + 3 + 5) + 8 => 1
+
+ expect(aggregated_metrics_data).to eq(results)
+ end
+ end
+
+ context 'with OR operator' do
+ let(:aggregated_metrics) do
+ [
+ { name: 'gmau_1', source: 'redis', events: %w[event3 event5], operator: "OR" },
+ { name: 'gmau_2', source: 'database', events: %w[event1 event2 event3], operator: "OR" }
+ ].map(&:with_indifferent_access)
+ end
+
+ it 'returns the number of unique events occurred for any metric in aggregate', :aggregate_failures do
+ results = {
+ 'gmau_1' => 5,
+ 'gmau_2' => 3
+ }
+ params = { start_date: start_date, end_date: end_date, recorded_at: recorded_at }
+
+ expect(sources::RedisHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event3 event5])).and_return(5)
+ expect(sources::PostgresHll).to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event1 event2 event3])).and_return(3)
+ 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
+ [
+ # represents stable aggregated metrics that has been fully released
+ { name: 'gmau_without_ff', source: 'redis', events: %w[event3_slot event5_slot], operator: "OR" },
+ # represents new aggregated metric that is under performance testing on gitlab.com
+ { name: 'gmau_enabled', source: 'redis', events: %w[event4], operator: "OR", feature_flag: enabled_feature_flag },
+ # represents aggregated metric that is under development and shouldn't be yet collected even on gitlab.com
+ { name: 'gmau_disabled', source: 'redis', events: %w[event4], operator: "OR", feature_flag: disabled_feature_flag }
+ ].map(&:with_indifferent_access)
+ 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(sources::RedisHll).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([{ name: 'gmau_1', source: 'redis', events: %w[event1_slot], operator: "SUM" }])
+ end
+
+ expect { aggregated_metrics_data }.to raise_error Gitlab::Usage::Metrics::Aggregates::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([{ name: 'gmau_1', source: 'whoami', events: %w[event1_slot], operator: "AND" }])
+ end
+
+ expect { aggregated_metrics_data }.to raise_error Gitlab::Usage::Metrics::Aggregates::UnknownAggregationSource
+ end
+
+ it 're raises Gitlab::UsageDataCounters::HLLRedisCounter::EventError' do
+ error = Gitlab::UsageDataCounters::HLLRedisCounter::EventError
+ allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:calculate_events_union).and_raise(error)
+
+ allow_next_instance_of(described_class) do |instance|
+ allow(instance).to receive(:aggregated_metrics)
+ .and_return([{ name: 'gmau_1', source: 'redis', events: %w[event1_slot], operator: "OR" }])
+ end
+
+ expect { aggregated_metrics_data }.to raise_error error
+ end
+ 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([{ name: 'gmau_1', source: 'redis', events: %w[event1_slot], operator: "SUM" }])
+ 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([{ name: 'gmau_1', source: 'whoami', events: %w[event1_slot], operator: "AND" }])
+ end
+
+ expect(aggregated_metrics_data).to eq('gmau_1' => -1)
+ end
+
+ it 'rescues Gitlab::UsageDataCounters::HLLRedisCounter::EventError' do
+ error = Gitlab::UsageDataCounters::HLLRedisCounter::EventError
+ allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:calculate_events_union).and_raise(error)
+
+ allow_next_instance_of(described_class) do |instance|
+ allow(instance).to receive(:aggregated_metrics)
+ .and_return([{ name: 'gmau_1', source: 'redis', events: %w[event1_slot], operator: "OR" }])
+ end
+
+ expect(aggregated_metrics_data).to eq('gmau_1' => -1)
+ end
+ end
+ end
+ end
+
+ it 'allows for YAML aliases in aggregated metrics configs' do
+ expect(YAML).to receive(:safe_load).with(kind_of(String), aliases: true)
+
+ described_class.new(recorded_at)
+ 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 }
+
+ it_behaves_like 'aggregated_metrics_data'
+ 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 }
+
+ it_behaves_like 'aggregated_metrics_data'
+
+ context 'metrics union calls' do
+ let(:aggregated_metrics) do
+ [
+ { name: 'gmau_3', source: 'redis', events: %w[event1_slot event2_slot event3_slot event5_slot], operator: "AND" }
+ ].map(&:with_indifferent_access)
+ end
+
+ it 'caches intermediate operations', :aggregate_failures do
+ allow_next_instance_of(described_class) do |instance|
+ allow(instance).to receive(:aggregated_metrics).and_return(aggregated_metrics)
+ end
+
+ params = { start_date: start_date, end_date: end_date, recorded_at: recorded_at }
+
+ aggregated_metrics[0][:events].each do |event|
+ expect(sources::RedisHll).to receive(:calculate_metrics_union)
+ .with(params.merge(metric_names: event))
+ .once
+ .and_return(0)
+ end
+
+ 2.upto(4) do |subset_size|
+ aggregated_metrics[0][:events].combination(subset_size).each do |events|
+ expect(sources::RedisHll).to receive(:calculate_metrics_union)
+ .with(params.merge(metric_names: events))
+ .once
+ .and_return(0)
+ end
+ end
+
+ aggregated_metrics_data
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/usage/metrics/aggregates/sources/postgres_hll_spec.rb b/spec/lib/gitlab/usage/metrics/aggregates/sources/postgres_hll_spec.rb
new file mode 100644
index 00000000000..7b8be8e8bc6
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/aggregates/sources/postgres_hll_spec.rb
@@ -0,0 +1,146 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Aggregates::Sources::PostgresHll, :clean_gitlab_redis_shared_state do
+ let_it_be(:start_date) { 7.days.ago }
+ let_it_be(:end_date) { Date.current }
+ let_it_be(:recorded_at) { Time.current }
+ let_it_be(:time_period) { { created_at: (start_date..end_date) } }
+ let(:metric_1) { 'metric_1' }
+ let(:metric_2) { 'metric_2' }
+ let(:metric_names) { [metric_1, metric_2] }
+
+ describe '.calculate_events_union' do
+ subject(:calculate_metrics_union) do
+ described_class.calculate_metrics_union(metric_names: metric_names, start_date: start_date, end_date: end_date, recorded_at: recorded_at)
+ end
+
+ before do
+ [
+ {
+ metric_name: metric_1,
+ time_period: time_period,
+ recorded_at_timestamp: recorded_at,
+ data: ::Gitlab::Database::PostgresHll::Buckets.new(141 => 1, 56 => 1)
+ },
+ {
+ metric_name: metric_2,
+ time_period: time_period,
+ recorded_at_timestamp: recorded_at,
+ data: ::Gitlab::Database::PostgresHll::Buckets.new(10 => 1, 56 => 1)
+ }
+ ].each do |params|
+ described_class.save_aggregated_metrics(**params)
+ end
+ end
+
+ it 'returns the number of unique events in the union of all metrics' do
+ expect(calculate_metrics_union.round(2)).to eq(3.12)
+ end
+
+ context 'when there is no aggregated data saved' do
+ let(:metric_names) { [metric_1, 'i do not have any records'] }
+
+ it 'raises error when union data is missing' do
+ expect { calculate_metrics_union }.to raise_error Gitlab::Usage::Metrics::Aggregates::Sources::UnionNotAvailable
+ end
+ end
+
+ context 'when there is only one metric defined as aggregated' do
+ let(:metric_names) { [metric_1] }
+
+ it 'returns the number of unique events for that metric' do
+ expect(calculate_metrics_union.round(2)).to eq(2.08)
+ end
+ end
+ end
+
+ describe '.save_aggregated_metrics' do
+ subject(:save_aggregated_metrics) do
+ described_class.save_aggregated_metrics(metric_name: metric_1,
+ time_period: time_period,
+ recorded_at_timestamp: recorded_at,
+ data: data)
+ end
+
+ context 'with compatible data argument' do
+ let(:data) { ::Gitlab::Database::PostgresHll::Buckets.new(141 => 1, 56 => 1) }
+
+ it 'persists serialized data in Redis' do
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis).to receive(:set).with("#{metric_1}_weekly-#{recorded_at.to_i}", '{"141":1,"56":1}', ex: 120.hours)
+ end
+
+ save_aggregated_metrics
+ end
+
+ context 'with monthly key' do
+ let_it_be(:start_date) { 4.weeks.ago }
+ let_it_be(:time_period) { { created_at: (start_date..end_date) } }
+
+ it 'persists serialized data in Redis' do
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis).to receive(:set).with("#{metric_1}_monthly-#{recorded_at.to_i}", '{"141":1,"56":1}', ex: 120.hours)
+ end
+
+ save_aggregated_metrics
+ end
+ end
+
+ context 'with all_time key' do
+ let_it_be(:time_period) { nil }
+
+ it 'persists serialized data in Redis' do
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis).to receive(:set).with("#{metric_1}_all_time-#{recorded_at.to_i}", '{"141":1,"56":1}', ex: 120.hours)
+ end
+
+ save_aggregated_metrics
+ end
+ end
+
+ context 'error handling' do
+ before do
+ allow(Gitlab::Redis::SharedState).to receive(:with).and_raise(::Redis::CommandError)
+ end
+
+ it 'rescues and reraise ::Redis::CommandError for development and test environments' do
+ expect { save_aggregated_metrics }.to raise_error ::Redis::CommandError
+ end
+
+ context 'for environment different than development' do
+ before do
+ stub_rails_env('production')
+ end
+
+ it 'rescues ::Redis::CommandError' do
+ expect { save_aggregated_metrics }.not_to raise_error
+ end
+ end
+ end
+ end
+
+ context 'with incompatible data argument' do
+ let(:data) { 1 }
+
+ context 'for environment different than development' do
+ before do
+ stub_rails_env('production')
+ end
+
+ it 'does not persist data in Redis' do
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis).not_to receive(:set)
+ end
+
+ save_aggregated_metrics
+ end
+ end
+
+ it 'raises error for development environment' do
+ expect { save_aggregated_metrics }.to raise_error /Unsupported data type/
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/usage/metrics/aggregates/sources/redis_hll_spec.rb b/spec/lib/gitlab/usage/metrics/aggregates/sources/redis_hll_spec.rb
new file mode 100644
index 00000000000..af2de5ea343
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/aggregates/sources/redis_hll_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Aggregates::Sources::RedisHll do
+ describe '.calculate_events_union' do
+ let(:event_names) { %w[event_a event_b] }
+ let(:start_date) { 7.days.ago }
+ let(:end_date) { Date.current }
+
+ subject(:calculate_metrics_union) do
+ described_class.calculate_metrics_union(metric_names: event_names, start_date: start_date, end_date: end_date, recorded_at: nil)
+ end
+
+ it 'calls Gitlab::UsageDataCounters::HLLRedisCounter.calculate_events_union' do
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:calculate_events_union)
+ .with(event_names: event_names, start_date: start_date, end_date: end_date)
+ .and_return(5)
+
+ calculate_metrics_union
+ end
+
+ it 'prevents from using fallback value as valid union result' do
+ allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:calculate_events_union).and_return(-1)
+
+ expect { calculate_metrics_union }.to raise_error Gitlab::Usage::Metrics::Aggregates::Sources::UnionNotAvailable
+ end
+ end
+end
diff --git a/spec/lib/gitlab/usage_data_counters/aggregated_metrics_spec.rb b/spec/lib/gitlab/usage_data_counters/aggregated_metrics_spec.rb
index c0deb2aa00c..58f974fbe12 100644
--- a/spec/lib/gitlab/usage_data_counters/aggregated_metrics_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/aggregated_metrics_spec.rb
@@ -13,18 +13,32 @@ RSpec.describe 'aggregated metrics' do
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
+
let_it_be(:known_events) do
Gitlab::UsageDataCounters::HLLRedisCounter.known_events
end
- Gitlab::UsageDataCounters::HLLRedisCounter.aggregated_metrics.tap do |aggregated_metrics|
+ 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
- aggregated_metrics&.each do |aggregate|
+ it 'all aggregated metrics has known source' do
+ expect(aggregated_metrics).to all has_known_source
+ 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]) } }
@@ -37,7 +51,7 @@ RSpec.describe 'aggregated metrics' do
end
it "uses allowed aggregation operators" do
- expect(Gitlab::UsageDataCounters::HLLRedisCounter::ALLOWED_METRICS_AGGREGATIONS).to include aggregate[:operator]
+ expect(Gitlab::Usage::Metrics::Aggregates::ALLOWED_METRICS_AGGREGATIONS).to include aggregate[:operator]
end
it "uses events from the same Redis slot" do
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 ba7bfe47bc9..b1d5d106082 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
@@ -3,28 +3,88 @@
require 'spec_helper'
RSpec.describe Gitlab::UsageDataCounters::CiTemplateUniqueCounter do
- let(:project_id) { 1 }
-
describe '.track_unique_project_event' do
- described_class::TEMPLATE_TO_EVENT.keys.each do |template|
- context "when given template #{template}" do
- it_behaves_like 'tracking unique hll events', :usage_data_track_ci_templates_unique_projects do
- subject(:request) { described_class.track_unique_project_event(project_id: project_id, template: template) }
+ using RSpec::Parameterized::TableSyntax
+
+ where(:template, :config_source, :expected_event) do
+ # Implicit Auto DevOps usage
+ 'Auto-DevOps.gitlab-ci.yml' | :auto_devops_source | 'p_ci_templates_implicit_auto_devops'
+ 'Jobs/Build.gitlab-ci.yml' | :auto_devops_source | 'p_ci_templates_implicit_auto_devops_build'
+ 'Jobs/Deploy.gitlab-ci.yml' | :auto_devops_source | 'p_ci_templates_implicit_auto_devops_deploy'
+ 'Security/SAST.gitlab-ci.yml' | :auto_devops_source | 'p_ci_templates_implicit_security_sast'
+ 'Security/Secret-Detection.gitlab-ci.yml' | :auto_devops_source | 'p_ci_templates_implicit_security_secret_detection'
+ # Explicit include:template usage
+ '5-Minute-Production-App.gitlab-ci.yml' | :repository_source | 'p_ci_templates_5_min_production_app'
+ 'Auto-DevOps.gitlab-ci.yml' | :repository_source | 'p_ci_templates_auto_devops'
+ 'AWS/CF-Provision-and-Deploy-EC2.gitlab-ci.yml' | :repository_source | 'p_ci_templates_aws_cf_deploy_ec2'
+ 'AWS/Deploy-ECS.gitlab-ci.yml' | :repository_source | 'p_ci_templates_aws_deploy_ecs'
+ 'Jobs/Build.gitlab-ci.yml' | :repository_source | 'p_ci_templates_auto_devops_build'
+ 'Jobs/Deploy.gitlab-ci.yml' | :repository_source | 'p_ci_templates_auto_devops_deploy'
+ 'Jobs/Deploy.latest.gitlab-ci.yml' | :repository_source | 'p_ci_templates_auto_devops_deploy_latest'
+ 'Security/SAST.gitlab-ci.yml' | :repository_source | 'p_ci_templates_security_sast'
+ 'Security/Secret-Detection.gitlab-ci.yml' | :repository_source | 'p_ci_templates_security_secret_detection'
+ 'Terraform/Base.latest.gitlab-ci.yml' | :repository_source | 'p_ci_templates_terraform_base_latest'
+ end
+
+ with_them do
+ it_behaves_like 'tracking unique hll events' do
+ subject(:request) { described_class.track_unique_project_event(project_id: project_id, template: template, config_source: config_source) }
+
+ let(:project_id) { 1 }
+ let(:target_id) { expected_event }
+ let(:expected_type) { instance_of(Integer) }
+ end
+ end
+
+ context 'known_events coverage tests' do
+ let(:project_id) { 1 }
+ let(:config_source) { :repository_source }
- let(:target_id) { "p_ci_templates_#{described_class::TEMPLATE_TO_EVENT[template]}" }
- let(:expected_type) { instance_of(Integer) }
+ # These tests help guard against missing "explicit" events in known_events/ci_templates.yml
+ context 'explicit include:template events' do
+ described_class::TEMPLATE_TO_EVENT.keys.each do |template|
+ it "does not raise error for #{template}" do
+ expect do
+ described_class.track_unique_project_event(project_id: project_id, template: template, config_source: config_source)
+ end.not_to raise_error
+ end
+ end
+ end
+
+ # This test is to help guard against missing "implicit" events in known_events/ci_templates.yml
+ it 'does not raise error for any template in an implicit Auto DevOps pipeline' do
+ project = create(:project, :auto_devops)
+ pipeline = double(project: project)
+ command = double
+ result = Gitlab::Ci::YamlProcessor.new(
+ Gitlab::Ci::Pipeline::Chain::Config::Content::AutoDevops.new(pipeline, command).content,
+ project: project,
+ user: double,
+ sha: double
+ ).execute
+
+ config_source = :auto_devops_source
+
+ result.included_templates.each do |template|
+ expect do
+ described_class.track_unique_project_event(project_id: project.id, template: template, config_source: config_source)
+ end.not_to raise_error
end
end
end
- it 'does not track templates outside of TEMPLATE_TO_EVENT' do
- expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to(
- receive(:track_event)
- )
+ context 'templates outside of TEMPLATE_TO_EVENT' do
+ let(:project_id) { 1 }
+ let(:config_source) { :repository_source }
+
Dir.glob(File.join('lib', 'gitlab', 'ci', 'templates', '**'), base: Rails.root) do |template|
next if described_class::TEMPLATE_TO_EVENT.key?(template)
- described_class.track_unique_project_event(project_id: 1, template: template)
+ it "does not track #{template}" do
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to(receive(:track_event))
+
+ described_class.track_unique_project_event(project_id: project_id, template: template, config_source: config_source)
+ end
end
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 b8eddc0ca7f..b4894ec049f 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
@@ -27,6 +27,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
'deploy_token_packages',
'user_packages',
'compliance',
+ 'ecosystem',
'analytics',
'ide_edit',
'search',
@@ -39,12 +40,16 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
'snippets',
'code_review',
'terraform',
- 'ci_templates'
+ 'ci_templates',
+ 'quickactions',
+ 'pipeline_authoring'
)
end
end
describe 'known_events' do
+ let(:feature) { 'test_hll_redis_counter_ff_check' }
+
let(:weekly_event) { 'g_analytics_contribution' }
let(:daily_event) { 'g_analytics_search' }
let(:analytics_slot_event) { 'g_analytics_contribution' }
@@ -64,7 +69,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
let(:known_events) do
[
- { name: weekly_event, redis_slot: "analytics", category: analytics_category, expiry: 84, aggregation: "weekly" },
+ { name: weekly_event, redis_slot: "analytics", category: analytics_category, expiry: 84, aggregation: "weekly", feature_flag: feature },
{ name: daily_event, redis_slot: "analytics", category: analytics_category, expiry: 84, aggregation: "daily" },
{ name: category_productivity_event, redis_slot: "analytics", category: productivity_category, aggregation: "weekly" },
{ name: compliance_slot_event, redis_slot: "compliance", category: compliance_category, aggregation: "weekly" },
@@ -75,6 +80,8 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
end
before do
+ skip_feature_flags_yaml_validation
+ skip_default_enabled_yaml_check
allow(described_class).to receive(:known_events).and_return(known_events)
end
@@ -85,6 +92,32 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
end
describe '.track_event' do
+ context 'with feature flag set' do
+ it 'tracks the event when feature enabled' do
+ stub_feature_flags(feature => true)
+
+ expect(Gitlab::Redis::HLL).to receive(:add)
+
+ described_class.track_event(weekly_event, values: 1)
+ end
+
+ it 'does not track the event with feature flag disabled' do
+ stub_feature_flags(feature => false)
+
+ expect(Gitlab::Redis::HLL).not_to receive(:add)
+
+ described_class.track_event(weekly_event, values: 1)
+ end
+ end
+
+ context 'with no feature flag set' do
+ it 'tracks the event' do
+ expect(Gitlab::Redis::HLL).to receive(:add)
+
+ described_class.track_event(daily_event, values: 1)
+ end
+ end
+
context 'when usage_ping is disabled' do
it 'does not track the event' do
stub_application_setting(usage_ping_enabled: false)
@@ -425,182 +458,59 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
end
end
- context 'aggregated_metrics_data' do
+ describe '.calculate_events_union' do
+ let(:time_range) { { start_date: 7.days.ago, end_date: DateTime.current } }
let(:known_events) do
[
{ name: 'event1_slot', redis_slot: "slot", category: 'category1', aggregation: "weekly" },
{ name: 'event2_slot', redis_slot: "slot", category: 'category2', aggregation: "weekly" },
{ name: 'event3_slot', redis_slot: "slot", category: 'category3', aggregation: "weekly" },
- { name: 'event5_slot', redis_slot: "slot", category: 'category4', aggregation: "weekly" },
+ { name: 'event5_slot', redis_slot: "slot", category: 'category4', aggregation: "daily" },
{ name: 'event4', category: 'category2', aggregation: "weekly" }
].map(&:with_indifferent_access)
end
before do
allow(described_class).to receive(:known_events).and_return(known_events)
- end
-
- shared_examples 'aggregated_metrics_data' do
- context 'no aggregated metrics is defined' do
- it 'returns empty hash' do
- allow(described_class).to receive(:aggregated_metrics).and_return([])
-
- expect(aggregated_metrics_data).to eq({})
- end
- end
-
- context 'there are aggregated metrics defined' do
- before do
- allow(described_class).to receive(:aggregated_metrics).and_return(aggregated_metrics)
- end
-
- context 'with AND operator' do
- let(:aggregated_metrics) do
- [
- { name: 'gmau_1', events: %w[event1_slot event2_slot], operator: "AND" },
- { name: 'gmau_2', events: %w[event1_slot event2_slot event3_slot], operator: "AND" },
- { name: 'gmau_3', events: %w[event1_slot event2_slot event3_slot event5_slot], operator: "AND" },
- { name: 'gmau_4', events: %w[event4], operator: "AND" }
- ].map(&:with_indifferent_access)
- end
-
- it 'returns the number of unique events for all known events' do
- results = {
- 'gmau_1' => 3,
- 'gmau_2' => 2,
- 'gmau_3' => 1,
- 'gmau_4' => 3
- }
-
- expect(aggregated_metrics_data).to eq(results)
- end
- end
-
- context 'with OR operator' do
- let(:aggregated_metrics) do
- [
- { name: 'gmau_1', events: %w[event3_slot event5_slot], operator: "OR" },
- { name: 'gmau_2', events: %w[event1_slot event2_slot event3_slot event5_slot], operator: "OR" },
- { name: 'gmau_3', events: %w[event4], operator: "OR" }
- ].map(&:with_indifferent_access)
- end
- it 'returns the number of unique events for all known events' do
- results = {
- 'gmau_1' => 2,
- 'gmau_2' => 3,
- 'gmau_3' => 3
- }
-
- 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
- [
- # represents stable aggregated metrics that has been fully released
- { name: 'gmau_without_ff', events: %w[event3_slot event5_slot], operator: "OR" },
- # represents new aggregated metric that is under performance testing on gitlab.com
- { name: 'gmau_enabled', events: %w[event4], operator: "AND", feature_flag: enabled_feature_flag },
- # represents aggregated metric that is under development and shouldn't be yet collected even on gitlab.com
- { name: 'gmau_disabled', events: %w[event4], operator: "AND", feature_flag: disabled_feature_flag }
- ].map(&:with_indifferent_access)
- end
-
- it 'returns the number of unique events for all known events' do
- skip_feature_flags_yaml_validation
- stub_feature_flags(enabled_feature_flag => true, disabled_feature_flag => false)
+ described_class.track_event('event1_slot', values: entity1, time: 2.days.ago)
+ described_class.track_event('event1_slot', values: entity2, time: 2.days.ago)
+ described_class.track_event('event1_slot', values: entity3, time: 2.days.ago)
+ described_class.track_event('event2_slot', values: entity1, time: 2.days.ago)
+ described_class.track_event('event2_slot', values: entity2, time: 3.days.ago)
+ described_class.track_event('event2_slot', values: entity3, time: 3.days.ago)
+ described_class.track_event('event3_slot', values: entity1, time: 3.days.ago)
+ described_class.track_event('event3_slot', values: entity2, time: 3.days.ago)
+ described_class.track_event('event5_slot', values: entity2, time: 3.days.ago)
+
+ # events out of time scope
+ described_class.track_event('event2_slot', values: entity4, time: 8.days.ago)
- expect(aggregated_metrics_data).to eq('gmau_without_ff' => 2, 'gmau_enabled' => 3)
- end
- end
- end
+ # events in different slots
+ described_class.track_event('event4', values: entity1, time: 2.days.ago)
+ described_class.track_event('event4', values: entity2, time: 2.days.ago)
end
- describe '.aggregated_metrics_weekly_data' do
- subject(:aggregated_metrics_data) { described_class.aggregated_metrics_weekly_data }
-
- before do
- described_class.track_event('event1_slot', values: entity1, time: 2.days.ago)
- described_class.track_event('event1_slot', values: entity2, time: 2.days.ago)
- described_class.track_event('event1_slot', values: entity3, time: 2.days.ago)
- described_class.track_event('event2_slot', values: entity1, time: 2.days.ago)
- described_class.track_event('event2_slot', values: entity2, time: 3.days.ago)
- described_class.track_event('event2_slot', values: entity3, time: 3.days.ago)
- described_class.track_event('event3_slot', values: entity1, time: 3.days.ago)
- described_class.track_event('event3_slot', values: entity2, time: 3.days.ago)
- described_class.track_event('event5_slot', values: entity2, time: 3.days.ago)
-
- # events out of time scope
- described_class.track_event('event2_slot', values: entity3, time: 8.days.ago)
-
- # events in different slots
- described_class.track_event('event4', values: entity1, time: 2.days.ago)
- described_class.track_event('event4', values: entity2, time: 2.days.ago)
- described_class.track_event('event4', values: entity4, time: 2.days.ago)
- end
-
- it_behaves_like 'aggregated_metrics_data'
+ it 'calculates union of given events', :aggregate_failure do
+ expect(described_class.calculate_events_union(**time_range.merge(event_names: %w[event4]))).to eq 2
+ expect(described_class.calculate_events_union(**time_range.merge(event_names: %w[event1_slot event2_slot event3_slot]))).to eq 3
end
- describe '.aggregated_metrics_monthly_data' do
- subject(:aggregated_metrics_data) { described_class.aggregated_metrics_monthly_data }
-
- it_behaves_like 'aggregated_metrics_data' do
- before do
- described_class.track_event('event1_slot', values: entity1, time: 2.days.ago)
- described_class.track_event('event1_slot', values: entity2, time: 2.days.ago)
- described_class.track_event('event1_slot', values: entity3, time: 2.days.ago)
- described_class.track_event('event2_slot', values: entity1, time: 2.days.ago)
- described_class.track_event('event2_slot', values: entity2, time: 3.days.ago)
- described_class.track_event('event2_slot', values: entity3, time: 3.days.ago)
- described_class.track_event('event3_slot', values: entity1, time: 3.days.ago)
- described_class.track_event('event3_slot', values: entity2, time: 10.days.ago)
- described_class.track_event('event5_slot', values: entity2, time: 4.weeks.ago.advance(days: 1))
-
- # events out of time scope
- described_class.track_event('event5_slot', values: entity1, time: 4.weeks.ago.advance(days: -1))
-
- # events in different slots
- described_class.track_event('event4', values: entity1, time: 2.days.ago)
- described_class.track_event('event4', values: entity2, time: 2.days.ago)
- described_class.track_event('event4', values: entity4, time: 2.days.ago)
- end
- end
-
- context 'Redis calls' do
- let(:aggregated_metrics) do
- [
- { name: 'gmau_3', events: %w[event1_slot event2_slot event3_slot event5_slot], operator: "AND" }
- ].map(&:with_indifferent_access)
- end
-
- let(:known_events) do
- [
- { name: 'event1_slot', redis_slot: "slot", category: 'category1', aggregation: "weekly" },
- { name: 'event2_slot', redis_slot: "slot", category: 'category2', aggregation: "weekly" },
- { name: 'event3_slot', redis_slot: "slot", category: 'category3', aggregation: "weekly" },
- { name: 'event5_slot', redis_slot: "slot", category: 'category4', aggregation: "weekly" }
- ].map(&:with_indifferent_access)
- end
-
- it 'caches intermediate operations' do
- allow(described_class).to receive(:known_events).and_return(known_events)
- allow(described_class).to receive(:aggregated_metrics).and_return(aggregated_metrics)
+ it 'validates and raise exception if events has mismatched slot or aggregation', :aggregate_failure do
+ expect { described_class.calculate_events_union(**time_range.merge(event_names: %w[event1_slot event4])) }.to raise_error described_class::SlotMismatch
+ expect { described_class.calculate_events_union(**time_range.merge(event_names: %w[event5_slot event3_slot])) }.to raise_error described_class::AggregationMismatch
+ end
+ end
- 4.downto(1) do |subset_size|
- known_events.combination(subset_size).each do |events|
- keys = described_class.send(:weekly_redis_keys, events: events, start_date: 4.weeks.ago.to_date, end_date: Date.current)
- expect(Gitlab::Redis::HLL).to receive(:count).with(keys: keys).once.and_return(0)
- end
- end
+ describe '.weekly_time_range' do
+ it 'return hash with weekly time range boundaries' do
+ expect(described_class.weekly_time_range).to eq(start_date: 7.days.ago.to_date, end_date: Date.current)
+ end
+ end
- subject
- end
- end
+ describe '.monthly_time_range' do
+ it 'return hash with monthly time range boundaries' do
+ expect(described_class.monthly_time_range).to eq(start_date: 4.weeks.ago.to_date, end_date: Date.current)
end
end
end
diff --git a/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb
index c7b208cfb31..a604de4a61f 100644
--- a/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb
@@ -73,6 +73,54 @@ RSpec.describe Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter, :cl
end
end
+ describe '.track_approve_mr_action' do
+ subject { described_class.track_approve_mr_action(user: user) }
+
+ it_behaves_like 'a tracked merge request unique event' do
+ let(:action) { described_class::MR_APPROVE_ACTION }
+ end
+ end
+
+ describe '.track_unapprove_mr_action' do
+ subject { described_class.track_unapprove_mr_action(user: user) }
+
+ it_behaves_like 'a tracked merge request unique event' do
+ let(:action) { described_class::MR_UNAPPROVE_ACTION }
+ end
+ end
+
+ describe '.track_resolve_thread_action' do
+ subject { described_class.track_resolve_thread_action(user: user) }
+
+ it_behaves_like 'a tracked merge request unique event' do
+ let(:action) { described_class::MR_RESOLVE_THREAD_ACTION }
+ end
+ end
+
+ describe '.track_unresolve_thread_action' do
+ subject { described_class.track_unresolve_thread_action(user: user) }
+
+ it_behaves_like 'a tracked merge request unique event' do
+ let(:action) { described_class::MR_UNRESOLVE_THREAD_ACTION }
+ end
+ end
+
+ describe '.track_title_edit_action' do
+ subject { described_class.track_title_edit_action(user: user) }
+
+ it_behaves_like 'a tracked merge request unique event' do
+ let(:action) { described_class::MR_EDIT_MR_TITLE_ACTION }
+ end
+ end
+
+ describe '.track_description_edit_action' do
+ subject { described_class.track_description_edit_action(user: user) }
+
+ it_behaves_like 'a tracked merge request unique event' do
+ let(:action) { described_class::MR_EDIT_MR_DESC_ACTION }
+ end
+ end
+
describe '.track_create_comment_action' do
subject { described_class.track_create_comment_action(note: note) }
@@ -148,4 +196,92 @@ RSpec.describe Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter, :cl
let(:action) { described_class::MR_PUBLISH_REVIEW_ACTION }
end
end
+
+ describe '.track_add_suggestion_action' do
+ subject { described_class.track_add_suggestion_action(user: user) }
+
+ it_behaves_like 'a tracked merge request unique event' do
+ let(:action) { described_class::MR_ADD_SUGGESTION_ACTION }
+ end
+ end
+
+ describe '.track_apply_suggestion_action' do
+ subject { described_class.track_apply_suggestion_action(user: user) }
+
+ it_behaves_like 'a tracked merge request unique event' do
+ let(:action) { described_class::MR_APPLY_SUGGESTION_ACTION }
+ end
+ end
+
+ describe '.track_users_assigned_to_mr' do
+ subject { described_class.track_users_assigned_to_mr(users: [user]) }
+
+ it_behaves_like 'a tracked merge request unique event' do
+ let(:action) { described_class::MR_ASSIGNED_USERS_ACTION }
+ end
+ end
+
+ describe '.track_marked_as_draft_action' do
+ subject { described_class.track_marked_as_draft_action(user: user) }
+
+ it_behaves_like 'a tracked merge request unique event' do
+ let(:action) { described_class::MR_MARKED_AS_DRAFT_ACTION }
+ end
+ end
+
+ describe '.track_unmarked_as_draft_action' do
+ subject { described_class.track_unmarked_as_draft_action(user: user) }
+
+ it_behaves_like 'a tracked merge request unique event' do
+ let(:action) { described_class::MR_UNMARKED_AS_DRAFT_ACTION }
+ end
+ end
+
+ describe '.track_task_item_status_changed' do
+ subject { described_class.track_task_item_status_changed(user: user) }
+
+ it_behaves_like 'a tracked merge request unique event' do
+ let(:action) { described_class::MR_TASK_ITEM_STATUS_CHANGED_ACTION }
+ end
+ end
+
+ describe '.track_users_review_requested' do
+ subject { described_class.track_users_review_requested(users: [user]) }
+
+ it_behaves_like 'a tracked merge request unique event' do
+ let(:action) { described_class::MR_REVIEW_REQUESTED_USERS_ACTION }
+ end
+ end
+
+ describe '.track_approval_rule_added_action' do
+ subject { described_class.track_approval_rule_added_action(user: user) }
+
+ it_behaves_like 'a tracked merge request unique event' do
+ let(:action) { described_class::MR_APPROVAL_RULE_ADDED_USERS_ACTION }
+ end
+ end
+
+ describe '.track_approval_rule_edited_action' do
+ subject { described_class.track_approval_rule_edited_action(user: user) }
+
+ it_behaves_like 'a tracked merge request unique event' do
+ let(:action) { described_class::MR_APPROVAL_RULE_EDITED_USERS_ACTION }
+ end
+ end
+
+ describe '.track_approval_rule_deleted_action' do
+ subject { described_class.track_approval_rule_deleted_action(user: user) }
+
+ it_behaves_like 'a tracked merge request unique event' do
+ let(:action) { described_class::MR_APPROVAL_RULE_DELETED_USERS_ACTION }
+ end
+ end
+
+ describe '.track_mr_create_from_issue' do
+ subject { described_class.track_mr_create_from_issue(user: user) }
+
+ it_behaves_like 'a tracked merge request unique event' do
+ let(:action) { described_class::MR_CREATE_FROM_ISSUE_ACTION }
+ end
+ end
end
diff --git a/spec/lib/gitlab/usage_data_counters/quick_action_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/quick_action_activity_unique_counter_spec.rb
new file mode 100644
index 00000000000..d4c423f57fe
--- /dev/null
+++ b/spec/lib/gitlab/usage_data_counters/quick_action_activity_unique_counter_spec.rb
@@ -0,0 +1,163 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::UsageDataCounters::QuickActionActivityUniqueCounter, :clean_gitlab_redis_shared_state do
+ let(:user) { build(:user, id: 1) }
+ let(:note) { build(:note, author: user) }
+ let(:args) { nil }
+
+ shared_examples_for 'a tracked quick action unique event' do
+ specify do
+ expect { 3.times { subject } }
+ .to change {
+ Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(
+ event_names: action,
+ start_date: 2.weeks.ago,
+ end_date: 2.weeks.from_now
+ )
+ }
+ .by(1)
+ end
+ end
+
+ subject { described_class.track_unique_action(quickaction_name, args: args, user: user) }
+
+ describe '.track_unique_action' do
+ let(:quickaction_name) { 'approve' }
+
+ it_behaves_like 'a tracked quick action unique event' do
+ let(:action) { 'i_quickactions_approve' }
+ end
+ end
+
+ context 'tracking assigns' do
+ let(:quickaction_name) { 'assign' }
+
+ context 'single assignee' do
+ let(:args) { '@one' }
+
+ it_behaves_like 'a tracked quick action unique event' do
+ let(:action) { 'i_quickactions_assign_single' }
+ end
+ end
+
+ context 'multiple assignees' do
+ let(:args) { '@one @two' }
+
+ it_behaves_like 'a tracked quick action unique event' do
+ let(:action) { 'i_quickactions_assign_multiple' }
+ end
+ end
+
+ context 'assigning "me"' do
+ let(:args) { 'me' }
+
+ it_behaves_like 'a tracked quick action unique event' do
+ let(:action) { 'i_quickactions_assign_self' }
+ end
+ end
+
+ context 'assigning a reviewer' do
+ let(:quickaction_name) { 'assign_reviewer' }
+
+ it_behaves_like 'a tracked quick action unique event' do
+ let(:action) { 'i_quickactions_assign_reviewer' }
+ end
+ end
+
+ context 'assigning a reviewer with request review alias' do
+ let(:quickaction_name) { 'request_review' }
+
+ it_behaves_like 'a tracked quick action unique event' do
+ let(:action) { 'i_quickactions_assign_reviewer' }
+ end
+ end
+ end
+
+ context 'tracking copy_metadata' do
+ let(:quickaction_name) { 'copy_metadata' }
+
+ context 'for issues' do
+ let(:args) { '#123' }
+
+ it_behaves_like 'a tracked quick action unique event' do
+ let(:action) { 'i_quickactions_copy_metadata_issue' }
+ end
+ end
+
+ context 'for merge requests' do
+ let(:args) { '!123' }
+
+ it_behaves_like 'a tracked quick action unique event' do
+ let(:action) { 'i_quickactions_copy_metadata_merge_request' }
+ end
+ end
+ end
+
+ context 'tracking spend' do
+ let(:quickaction_name) { 'spend' }
+
+ context 'adding time' do
+ let(:args) { '1d' }
+
+ it_behaves_like 'a tracked quick action unique event' do
+ let(:action) { 'i_quickactions_spend_add' }
+ end
+ end
+
+ context 'removing time' do
+ let(:args) { '-1d' }
+
+ it_behaves_like 'a tracked quick action unique event' do
+ let(:action) { 'i_quickactions_spend_subtract' }
+ end
+ end
+ end
+
+ context 'tracking unassign' do
+ let(:quickaction_name) { 'unassign' }
+
+ context 'unassigning everyone' do
+ it_behaves_like 'a tracked quick action unique event' do
+ let(:action) { 'i_quickactions_unassign_all' }
+ end
+ end
+
+ context 'unassigning specific users' do
+ let(:args) { '@hello' }
+
+ it_behaves_like 'a tracked quick action unique event' do
+ let(:action) { 'i_quickactions_unassign_specific' }
+ end
+ end
+ end
+
+ context 'tracking unlabel' do
+ context 'called as unlabel' do
+ let(:quickaction_name) { 'unlabel' }
+
+ context 'removing all labels' do
+ it_behaves_like 'a tracked quick action unique event' do
+ let(:action) { 'i_quickactions_unlabel_all' }
+ end
+ end
+
+ context 'removing specific labels' do
+ let(:args) { '~wow' }
+
+ it_behaves_like 'a tracked quick action unique event' do
+ let(:action) { 'i_quickactions_unlabel_specific' }
+ end
+ end
+ end
+
+ context 'called as remove_label' do
+ let(:quickaction_name) { 'remove_label' }
+
+ it_behaves_like 'a tracked quick action unique event' do
+ let(:action) { 'i_quickactions_unlabel_all' }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/usage_data_counters/vs_code_extenion_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/vs_code_extenion_activity_unique_counter_spec.rb
new file mode 100644
index 00000000000..7593d51fe76
--- /dev/null
+++ b/spec/lib/gitlab/usage_data_counters/vs_code_extenion_activity_unique_counter_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples 'a tracked vs code unique action' do |event|
+ before do
+ stub_application_setting(usage_ping_enabled: true)
+ end
+
+ def count_unique(date_from:, date_to:)
+ Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: action, start_date: date_from, end_date: date_to)
+ end
+
+ it 'tracks when the user agent is from vs code' do
+ aggregate_failures do
+ user_agent = { user_agent: 'vs-code-gitlab-workflow/3.11.1 VSCode/1.52.1 Node.js/12.14.1 (darwin; x64)' }
+
+ expect(track_action(user: user1, **user_agent)).to be_truthy
+ expect(track_action(user: user1, **user_agent)).to be_truthy
+ expect(track_action(user: user2, **user_agent)).to be_truthy
+
+ expect(count_unique(date_from: time - 1.week, date_to: time + 1.week)).to eq(2)
+ end
+ end
+
+ it 'does not track when the user agent is not from vs code' do
+ aggregate_failures do
+ user_agent = { user_agent: 'normal_user_agent' }
+
+ expect(track_action(user: user1, **user_agent)).to be_falsey
+ expect(track_action(user: user1, **user_agent)).to be_falsey
+ expect(track_action(user: user2, **user_agent)).to be_falsey
+
+ expect(count_unique(date_from: time - 1.week, date_to: time + 1.week)).to eq(0)
+ end
+ end
+
+ it 'does not track if user agent is not present' do
+ expect(track_action(user: nil, user_agent: nil)).to be_nil
+ end
+
+ it 'does not track if user is not present' do
+ user_agent = { user_agent: 'vs-code-gitlab-workflow/3.11.1 VSCode/1.52.1 Node.js/12.14.1 (darwin; x64)' }
+
+ expect(track_action(user: nil, **user_agent)).to be_nil
+ end
+end
+
+RSpec.describe Gitlab::UsageDataCounters::VSCodeExtensionActivityUniqueCounter, :clean_gitlab_redis_shared_state do
+ let(:user1) { build(:user, id: 1) }
+ let(:user2) { build(:user, id: 2) }
+ let(:time) { Time.current }
+
+ context 'when tracking a vs code api request' do
+ it_behaves_like 'a tracked vs code unique action' do
+ let(:action) { described_class::VS_CODE_API_REQUEST_ACTION }
+
+ def track_action(params)
+ described_class.track_api_request_when_trackable(**params)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index fd02521622c..602f6640d72 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -228,11 +228,32 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
)
end
- it 'includes imports usage data' do
+ it 'includes import gmau usage data' do
for_defined_days_back do
user = create(:user)
+ group = create(:group)
+ group.add_owner(user)
+
+ create(:project, import_type: :github, creator_id: user.id)
+ create(:jira_import_state, :finished, project: create(:project, creator_id: user.id))
+ create(:issue_csv_import, user: user)
+ create(:group_import_state, group: group, user: user)
create(:bulk_import, user: user)
+ end
+
+ expect(described_class.usage_activity_by_stage_manage({})).to include(
+ unique_users_all_imports: 10
+ )
+
+ expect(described_class.usage_activity_by_stage_manage(described_class.last_28_days_time_period)).to include(
+ unique_users_all_imports: 5
+ )
+ end
+
+ it 'includes imports usage data' do
+ for_defined_days_back do
+ user = create(:user)
%w(gitlab_project gitlab github bitbucket bitbucket_server gitea git manifest fogbugz phabricator).each do |type|
create(:project, import_type: type, creator_id: user.id)
@@ -242,72 +263,113 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
create(:jira_import_state, :finished, project: jira_project)
create(:issue_csv_import, user: user)
+
+ group = create(:group)
+ group.add_owner(user)
+ create(:group_import_state, group: group, user: user)
+
+ bulk_import = create(:bulk_import, user: user)
+ create(:bulk_import_entity, :group_entity, bulk_import: bulk_import)
+ create(:bulk_import_entity, :project_entity, bulk_import: bulk_import)
end
expect(described_class.usage_activity_by_stage_manage({})).to include(
{
bulk_imports: {
- gitlab: 2
+ gitlab_v1: 2,
+ gitlab: Gitlab::UsageData::DEPRECATED_VALUE
},
- projects_imported: {
- total: 2,
- gitlab_project: 2,
- gitlab: 2,
- github: 2,
+ project_imports: {
bitbucket: 2,
bitbucket_server: 2,
- gitea: 2,
git: 2,
+ gitea: 2,
+ github: 2,
+ gitlab: 2,
+ gitlab_migration: 2,
+ gitlab_project: 2,
manifest: 2
},
- issues_imported: {
+ issue_imports: {
jira: 2,
fogbugz: 2,
phabricator: 2,
csv: 2
- }
+ },
+ group_imports: {
+ group_import: 2,
+ gitlab_migration: 2
+ },
+ projects_imported: {
+ total: Gitlab::UsageData::DEPRECATED_VALUE,
+ gitlab_project: Gitlab::UsageData::DEPRECATED_VALUE,
+ gitlab: Gitlab::UsageData::DEPRECATED_VALUE,
+ github: Gitlab::UsageData::DEPRECATED_VALUE,
+ bitbucket: Gitlab::UsageData::DEPRECATED_VALUE,
+ bitbucket_server: Gitlab::UsageData::DEPRECATED_VALUE,
+ gitea: Gitlab::UsageData::DEPRECATED_VALUE,
+ git: Gitlab::UsageData::DEPRECATED_VALUE,
+ manifest: Gitlab::UsageData::DEPRECATED_VALUE
+ },
+ issues_imported: {
+ jira: Gitlab::UsageData::DEPRECATED_VALUE,
+ fogbugz: Gitlab::UsageData::DEPRECATED_VALUE,
+ phabricator: Gitlab::UsageData::DEPRECATED_VALUE,
+ csv: Gitlab::UsageData::DEPRECATED_VALUE
+ },
+ groups_imported: Gitlab::UsageData::DEPRECATED_VALUE
}
)
expect(described_class.usage_activity_by_stage_manage(described_class.last_28_days_time_period)).to include(
{
bulk_imports: {
- gitlab: 1
+ gitlab_v1: 1,
+ gitlab: Gitlab::UsageData::DEPRECATED_VALUE
},
- projects_imported: {
- total: 1,
- gitlab_project: 1,
- gitlab: 1,
- github: 1,
+ project_imports: {
bitbucket: 1,
bitbucket_server: 1,
- gitea: 1,
git: 1,
+ gitea: 1,
+ github: 1,
+ gitlab: 1,
+ gitlab_migration: 1,
+ gitlab_project: 1,
manifest: 1
},
- issues_imported: {
+ issue_imports: {
jira: 1,
fogbugz: 1,
phabricator: 1,
csv: 1
- }
+ },
+ group_imports: {
+ group_import: 1,
+ gitlab_migration: 1
+ },
+ projects_imported: {
+ total: Gitlab::UsageData::DEPRECATED_VALUE,
+ gitlab_project: Gitlab::UsageData::DEPRECATED_VALUE,
+ gitlab: Gitlab::UsageData::DEPRECATED_VALUE,
+ github: Gitlab::UsageData::DEPRECATED_VALUE,
+ bitbucket: Gitlab::UsageData::DEPRECATED_VALUE,
+ bitbucket_server: Gitlab::UsageData::DEPRECATED_VALUE,
+ gitea: Gitlab::UsageData::DEPRECATED_VALUE,
+ git: Gitlab::UsageData::DEPRECATED_VALUE,
+ manifest: Gitlab::UsageData::DEPRECATED_VALUE
+ },
+ issues_imported: {
+ jira: Gitlab::UsageData::DEPRECATED_VALUE,
+ fogbugz: Gitlab::UsageData::DEPRECATED_VALUE,
+ phabricator: Gitlab::UsageData::DEPRECATED_VALUE,
+ csv: Gitlab::UsageData::DEPRECATED_VALUE
+ },
+ groups_imported: Gitlab::UsageData::DEPRECATED_VALUE
+
}
)
end
- it 'includes group imports usage data' do
- for_defined_days_back do
- user = create(:user)
- group = create(:group)
- group.add_owner(user)
- create(:group_import_state, group: group, user: user)
- end
-
- expect(described_class.usage_activity_by_stage_manage({}))
- .to include(groups_imported: 2)
- expect(described_class.usage_activity_by_stage_manage(described_class.last_28_days_time_period))
- .to include(groups_imported: 1)
- end
-
def omniauth_providers
[
OpenStruct.new(name: 'google_oauth2'),
@@ -1262,7 +1324,9 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
subject { described_class.redis_hll_counters }
let(:categories) { ::Gitlab::UsageDataCounters::HLLRedisCounter.categories }
- let(:ineligible_total_categories) { %w[source_code ci_secrets_management incident_management_alerts snippets terraform] }
+ let(:ineligible_total_categories) do
+ %w[source_code ci_secrets_management incident_management_alerts snippets terraform pipeline_authoring]
+ end
it 'has all known_events' do
expect(subject).to have_key(:redis_hll_counters)
@@ -1286,8 +1350,10 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
describe '.aggregated_metrics_weekly' do
subject(:aggregated_metrics_payload) { described_class.aggregated_metrics_weekly }
- it 'uses ::Gitlab::UsageDataCounters::HLLRedisCounter#aggregated_metrics_data', :aggregate_failures do
- expect(::Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:aggregated_metrics_weekly_data).and_return(global_search_gmau: 123)
+ it 'uses ::Gitlab::Usage::Metrics::Aggregates::Aggregate#weekly_data', :aggregate_failures do
+ expect_next_instance_of(::Gitlab::Usage::Metrics::Aggregates::Aggregate) do |instance|
+ expect(instance).to receive(:weekly_data).and_return(global_search_gmau: 123)
+ end
expect(aggregated_metrics_payload).to eq(aggregated_metrics: { global_search_gmau: 123 })
end
end
@@ -1295,8 +1361,10 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
describe '.aggregated_metrics_monthly' do
subject(:aggregated_metrics_payload) { described_class.aggregated_metrics_monthly }
- it 'uses ::Gitlab::UsageDataCounters::HLLRedisCounter#aggregated_metrics_data', :aggregate_failures do
- expect(::Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:aggregated_metrics_monthly_data).and_return(global_search_gmau: 123)
+ it 'uses ::Gitlab::Usage::Metrics::Aggregates::Aggregate#monthly_data', :aggregate_failures do
+ expect_next_instance_of(::Gitlab::Usage::Metrics::Aggregates::Aggregate) do |instance|
+ expect(instance).to receive(:monthly_data).and_return(global_search_gmau: 123)
+ end
expect(aggregated_metrics_payload).to eq(aggregated_metrics: { global_search_gmau: 123 })
end
end
diff --git a/spec/lib/gitlab/utils/markdown_spec.rb b/spec/lib/gitlab/utils/markdown_spec.rb
index 93d91f7ed90..acc5bd47c8c 100644
--- a/spec/lib/gitlab/utils/markdown_spec.rb
+++ b/spec/lib/gitlab/utils/markdown_spec.rb
@@ -53,33 +53,23 @@ RSpec.describe Gitlab::Utils::Markdown do
end
context 'when string has a product suffix' do
- let(:string) { 'My Header (ULTIMATE)' }
-
- it 'ignores a product suffix' do
- is_expected.to eq 'my-header'
- end
-
- context 'with only modifier' do
- let(:string) { 'My Header (STARTER ONLY)' }
-
- it 'ignores a product suffix' do
- is_expected.to eq 'my-header'
- end
- end
-
- context 'with "*" around a product suffix' do
- let(:string) { 'My Header **(STARTER)**' }
-
- it 'ignores a product suffix' do
- is_expected.to eq 'my-header'
- end
- end
-
- context 'with "*" around a product suffix and only modifier' do
- let(:string) { 'My Header **(STARTER ONLY)**' }
-
- it 'ignores a product suffix' do
- is_expected.to eq 'my-header'
+ %w[CORE STARTER PREMIUM ULTIMATE FREE BRONZE SILVER GOLD].each do |tier|
+ ['', ' ONLY', ' SELF', ' SASS'].each do |modifier|
+ context "#{tier}#{modifier}" do
+ let(:string) { "My Header (#{tier}#{modifier})" }
+
+ it 'ignores a product suffix' do
+ is_expected.to eq 'my-header'
+ end
+
+ context 'with "*" around a product suffix' do
+ let(:string) { "My Header **(#{tier}#{modifier})**" }
+
+ it 'ignores a product suffix' do
+ is_expected.to eq 'my-header'
+ end
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/utils/override_spec.rb b/spec/lib/gitlab/utils/override_spec.rb
index 7ba7392df0f..a5e53c1dfc1 100644
--- a/spec/lib/gitlab/utils/override_spec.rb
+++ b/spec/lib/gitlab/utils/override_spec.rb
@@ -2,6 +2,9 @@
require 'fast_spec_helper'
+# Patching ActiveSupport::Concern
+require_relative '../../../../config/initializers/0_as_concern'
+
RSpec.describe Gitlab::Utils::Override do
let(:base) do
Struct.new(:good) do
@@ -164,6 +167,70 @@ RSpec.describe Gitlab::Utils::Override do
it_behaves_like 'checking as intended, nothing was overridden'
end
+
+ context 'when ActiveSupport::Concern and class_methods are used' do
+ # We need to give module names before using Override
+ let(:base) { stub_const('Base', Module.new) }
+ let(:extension) { stub_const('Extension', Module.new) }
+
+ def define_base(method_name:)
+ base.module_eval do
+ extend ActiveSupport::Concern
+
+ class_methods do
+ define_method(method_name) do
+ :f
+ end
+ end
+ end
+ end
+
+ def define_extension(method_name:)
+ extension.module_eval do
+ extend ActiveSupport::Concern
+
+ class_methods do
+ extend Gitlab::Utils::Override
+
+ override method_name
+ define_method(method_name) do
+ :g
+ end
+ end
+ end
+ end
+
+ context 'when it is defining a overriding method' do
+ before do
+ define_base(method_name: :f)
+ define_extension(method_name: :f)
+
+ base.prepend(extension)
+ end
+
+ it 'verifies' do
+ expect(base.f).to eq(:g)
+
+ described_class.verify!
+ end
+ end
+
+ context 'when it is not defining a overriding method' do
+ before do
+ define_base(method_name: :f)
+ define_extension(method_name: :g)
+
+ base.prepend(extension)
+ end
+
+ it 'raises NotImplementedError' do
+ expect(base.f).to eq(:f)
+
+ expect { described_class.verify! }
+ .to raise_error(NotImplementedError)
+ end
+ end
+ end
end
context 'when STATIC_VERIFICATION is not set' do
diff --git a/spec/lib/gitlab/utils/usage_data_spec.rb b/spec/lib/gitlab/utils/usage_data_spec.rb
index dfc381d0ef2..e964e695828 100644
--- a/spec/lib/gitlab/utils/usage_data_spec.rb
+++ b/spec/lib/gitlab/utils/usage_data_spec.rb
@@ -58,6 +58,16 @@ RSpec.describe Gitlab::Utils::UsageData do
expect(described_class.estimate_batch_distinct_count(relation, 'column')).to eq(5)
end
+ it 'yield provided block with PostgresHll::Buckets' do
+ buckets = Gitlab::Database::PostgresHll::Buckets.new
+
+ allow_next_instance_of(Gitlab::Database::PostgresHll::BatchDistinctCounter) do |instance|
+ allow(instance).to receive(:execute).and_return(buckets)
+ end
+
+ expect { |block| described_class.estimate_batch_distinct_count(relation, 'column', &block) }.to yield_with_args(buckets)
+ end
+
context 'quasi integration test for different counting parameters' do
# HyperLogLog http://algo.inria.fr/flajolet/Publications/FlFuGaMe07.pdf algorithm
# used in estimate_batch_distinct_count produce probabilistic
diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb
index 1052d4cbacc..665eebdfd9e 100644
--- a/spec/lib/gitlab/utils_spec.rb
+++ b/spec/lib/gitlab/utils_spec.rb
@@ -116,8 +116,6 @@ RSpec.describe Gitlab::Utils do
end
describe '.ms_to_round_sec' do
- using RSpec::Parameterized::TableSyntax
-
where(:original, :expected) do
1999.8999 | 1.9999
12384 | 12.384
@@ -169,8 +167,6 @@ RSpec.describe Gitlab::Utils do
end
describe '.remove_line_breaks' do
- using RSpec::Parameterized::TableSyntax
-
where(:original, :expected) do
"foo\nbar\nbaz" | "foobarbaz"
"foo\r\nbar\r\nbaz" | "foobarbaz"
@@ -281,8 +277,6 @@ RSpec.describe Gitlab::Utils do
end
describe '.append_path' do
- using RSpec::Parameterized::TableSyntax
-
where(:host, :path, :result) do
'http://test/' | '/foo/bar' | 'http://test/foo/bar'
'http://test/' | '//foo/bar' | 'http://test/foo/bar'
@@ -393,8 +387,6 @@ RSpec.describe Gitlab::Utils do
end
describe ".safe_downcase!" do
- using RSpec::Parameterized::TableSyntax
-
where(:str, :result) do
"test".freeze | "test"
"Test".freeze | "test"
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index 9662ad13631..c22df5dd063 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -15,9 +15,7 @@ RSpec.describe Gitlab::Workhorse do
end
before do
- allow(Feature::Gitaly).to receive(:server_feature_flags).and_return({
- 'gitaly-feature-foobar' => 'true'
- })
+ stub_feature_flags(gitaly_enforce_requests_limits: true)
end
describe ".send_git_archive" do
@@ -43,7 +41,7 @@ RSpec.describe Gitlab::Workhorse do
expect(command).to eq('git-archive')
expect(params).to eq({
'GitalyServer' => {
- features: { 'gitaly-feature-foobar' => 'true' },
+ features: { 'gitaly-feature-enforce-requests-limits' => 'true' },
address: Gitlab::GitalyClient.address(project.repository_storage),
token: Gitlab::GitalyClient.token(project.repository_storage)
},
@@ -73,7 +71,7 @@ RSpec.describe Gitlab::Workhorse do
expect(command).to eq('git-archive')
expect(params).to eq({
'GitalyServer' => {
- features: { 'gitaly-feature-foobar' => 'true' },
+ features: { 'gitaly-feature-enforce-requests-limits' => 'true' },
address: Gitlab::GitalyClient.address(project.repository_storage),
token: Gitlab::GitalyClient.token(project.repository_storage)
},
@@ -124,7 +122,7 @@ RSpec.describe Gitlab::Workhorse do
expect(command).to eq("git-format-patch")
expect(params).to eq({
'GitalyServer' => {
- features: { 'gitaly-feature-foobar' => 'true' },
+ features: { 'gitaly-feature-enforce-requests-limits' => 'true' },
address: Gitlab::GitalyClient.address(project.repository_storage),
token: Gitlab::GitalyClient.token(project.repository_storage)
},
@@ -187,7 +185,7 @@ RSpec.describe Gitlab::Workhorse do
expect(command).to eq("git-diff")
expect(params).to eq({
'GitalyServer' => {
- features: { 'gitaly-feature-foobar' => 'true' },
+ features: { 'gitaly-feature-enforce-requests-limits' => 'true' },
address: Gitlab::GitalyClient.address(project.repository_storage),
token: Gitlab::GitalyClient.token(project.repository_storage)
},
@@ -274,7 +272,7 @@ RSpec.describe Gitlab::Workhorse do
let(:gitaly_params) do
{
GitalyServer: {
- features: { 'gitaly-feature-foobar' => 'true' },
+ features: { 'gitaly-feature-enforce-requests-limits' => 'true' },
address: Gitlab::GitalyClient.address('default'),
token: Gitlab::GitalyClient.token('default')
}
@@ -310,6 +308,35 @@ RSpec.describe Gitlab::Workhorse do
it { is_expected.to include(ShowAllRefs: true) }
end
+
+ context 'when a feature flag is set for a single project' do
+ before do
+ stub_feature_flags(gitaly_mep_mep: project)
+ end
+
+ it 'sets the flag to true for that project' do
+ response = described_class.git_http_ok(repository, Gitlab::GlRepository::PROJECT, user, action)
+
+ expect(response.dig(:GitalyServer, :features)).to eq('gitaly-feature-enforce-requests-limits' => 'true',
+ 'gitaly-feature-mep-mep' => 'true')
+ end
+
+ it 'sets the flag to false for other projects' do
+ other_project = create(:project, :public, :repository)
+ response = described_class.git_http_ok(other_project.repository, Gitlab::GlRepository::PROJECT, user, action)
+
+ expect(response.dig(:GitalyServer, :features)).to eq('gitaly-feature-enforce-requests-limits' => 'true',
+ 'gitaly-feature-mep-mep' => 'false')
+ end
+
+ it 'sets the flag to false when there is no project' do
+ snippet = create(:personal_snippet, :repository)
+ response = described_class.git_http_ok(snippet.repository, Gitlab::GlRepository::SNIPPET, user, action)
+
+ expect(response.dig(:GitalyServer, :features)).to eq('gitaly-feature-enforce-requests-limits' => 'true',
+ 'gitaly-feature-mep-mep' => 'false')
+ end
+ end
end
context "when git_receive_pack action is passed" do
@@ -423,7 +450,7 @@ RSpec.describe Gitlab::Workhorse do
expect(command).to eq('git-blob')
expect(params).to eq({
'GitalyServer' => {
- features: { 'gitaly-feature-foobar' => 'true' },
+ features: { 'gitaly-feature-enforce-requests-limits' => 'true' },
address: Gitlab::GitalyClient.address(project.repository_storage),
token: Gitlab::GitalyClient.token(project.repository_storage)
},
@@ -485,7 +512,7 @@ RSpec.describe Gitlab::Workhorse do
expect(command).to eq('git-snapshot')
expect(params).to eq(
'GitalyServer' => {
- 'features' => { 'gitaly-feature-foobar' => 'true' },
+ 'features' => { 'gitaly-feature-enforce-requests-limits' => 'true' },
'address' => Gitlab::GitalyClient.address(project.repository_storage),
'token' => Gitlab::GitalyClient.token(project.repository_storage)
},
diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb
index e1b8323eb8e..5f945d5b9fc 100644
--- a/spec/lib/gitlab_spec.rb
+++ b/spec/lib/gitlab_spec.rb
@@ -95,6 +95,26 @@ RSpec.describe Gitlab do
end
end
+ describe '.com' do
+ subject { described_class.com { true } }
+
+ before do
+ allow(described_class).to receive(:com?).and_return(gl_com)
+ end
+
+ context 'when on GitLab.com' do
+ let(:gl_com) { true }
+
+ it { is_expected.to be true }
+ end
+
+ context 'when not on GitLab.com' do
+ let(:gl_com) { false }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
describe '.staging?' do
subject { described_class.staging? }
@@ -332,13 +352,13 @@ RSpec.describe Gitlab do
describe '.maintenance_mode?' do
it 'returns true when maintenance mode is enabled' do
- stub_application_setting(maintenance_mode: true)
+ stub_maintenance_mode_setting(true)
expect(described_class.maintenance_mode?).to eq(true)
end
it 'returns false when maintenance mode is disabled' do
- stub_application_setting(maintenance_mode: false)
+ stub_maintenance_mode_setting(false)
expect(described_class.maintenance_mode?).to eq(false)
end
diff --git a/spec/lib/object_storage/config_spec.rb b/spec/lib/object_storage/config_spec.rb
index 0ead2a1d269..1361d80fe75 100644
--- a/spec/lib/object_storage/config_spec.rb
+++ b/spec/lib/object_storage/config_spec.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
require 'rspec-parameterized'
-require 'fog/core'
RSpec.describe ObjectStorage::Config do
using RSpec::Parameterized::TableSyntax
@@ -34,7 +33,9 @@ RSpec.describe ObjectStorage::Config do
}
end
- subject { described_class.new(raw_config.as_json) }
+ subject do
+ described_class.new(raw_config.as_json)
+ end
describe '#load_provider' do
before do
@@ -45,6 +46,10 @@ RSpec.describe ObjectStorage::Config do
it 'registers AWS as a provider' do
expect(Fog.providers.keys).to include(:aws)
end
+
+ describe '#fog_connection' do
+ it { expect(subject.fog_connection).to be_a_kind_of(Fog::AWS::Storage::Real) }
+ end
end
context 'with Google' do
@@ -59,6 +64,10 @@ RSpec.describe ObjectStorage::Config do
it 'registers Google as a provider' do
expect(Fog.providers.keys).to include(:google)
end
+
+ describe '#fog_connection' do
+ it { expect(subject.fog_connection).to be_a_kind_of(Fog::Storage::GoogleXML::Real) }
+ end
end
context 'with Azure' do
@@ -73,6 +82,10 @@ RSpec.describe ObjectStorage::Config do
it 'registers AzureRM as a provider' do
expect(Fog.providers.keys).to include(:azurerm)
end
+
+ describe '#fog_connection' do
+ it { expect(subject.fog_connection).to be_a_kind_of(Fog::Storage::AzureRM::Real) }
+ end
end
end
@@ -170,6 +183,50 @@ RSpec.describe ObjectStorage::Config do
it { expect(subject.provider).to eq('AWS') }
it { expect(subject.aws?).to be true }
it { expect(subject.google?).to be false }
+
+ it 'returns the default S3 endpoint' do
+ subject.load_provider
+
+ expect(subject.s3_endpoint).to eq("https://test-bucket.s3.amazonaws.com")
+ end
+
+ describe 'with a custom endpoint' do
+ let(:endpoint) { 'https://my.example.com' }
+
+ before do
+ credentials[:endpoint] = endpoint
+ end
+
+ it 'returns the custom endpoint' do
+ subject.load_provider
+
+ expect(subject.s3_endpoint).to eq(endpoint)
+ end
+ end
+
+ context 'with custom S3 host and port' do
+ where(:host, :port, :scheme, :expected) do
+ 's3.example.com' | 8080 | nil | 'https://test-bucket.s3.example.com:8080'
+ 's3.example.com' | 443 | nil | 'https://test-bucket.s3.example.com'
+ 's3.example.com' | 443 | "https" | 'https://test-bucket.s3.example.com'
+ 's3.example.com' | nil | nil | 'https://test-bucket.s3.example.com'
+ 's3.example.com' | 80 | "http" | 'http://test-bucket.s3.example.com'
+ 's3.example.com' | "bogus" | nil | nil
+ end
+
+ with_them do
+ before do
+ credentials[:host] = host
+ credentials[:port] = port
+ credentials[:scheme] = scheme
+ subject.load_provider
+ end
+
+ it 'returns expected host' do
+ expect(subject.s3_endpoint).to eq(expected)
+ end
+ end
+ end
end
context 'with Google credentials' do
diff --git a/spec/lib/peek/views/external_http_spec.rb b/spec/lib/peek/views/external_http_spec.rb
new file mode 100644
index 00000000000..98c4f771f33
--- /dev/null
+++ b/spec/lib/peek/views/external_http_spec.rb
@@ -0,0 +1,215 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Peek::Views::ExternalHttp, :request_store do
+ subject { described_class.new }
+
+ let(:subscriber) { Gitlab::Metrics::Subscribers::ExternalHttp.new }
+
+ before do
+ allow(Gitlab::PerformanceBar).to receive(:enabled_for_request?).and_return(true)
+ end
+
+ let(:event_1) do
+ {
+ method: 'POST', code: "200", duration: 0.03,
+ scheme: 'https', host: 'gitlab.com', port: 80, path: '/api/v4/projects',
+ query: 'current=true'
+ }
+ end
+
+ let(:event_2) do
+ {
+ method: 'POST', duration: 1.3,
+ scheme: 'http', host: 'gitlab.com', port: 80, path: '/api/v4/projects/2/issues',
+ query: 'current=true',
+ exception_object: Net::ReadTimeout.new
+ }
+ end
+
+ let(:event_3) do
+ {
+ method: 'GET', code: "301", duration: 0.005,
+ scheme: 'http', host: 'gitlab.com', port: 80, path: '/api/v4/projects/2',
+ query: 'current=true',
+ proxy_host: 'proxy.gitlab.com', proxy_port: 8080
+ }
+ end
+
+ it 'returns no results' do
+ expect(subject.results).to eq(
+ calls: 0, details: [], duration: "0ms", warnings: []
+ )
+ end
+
+ it 'returns aggregated results' do
+ subscriber.request(double(:event, payload: event_1))
+ subscriber.request(double(:event, payload: event_2))
+ subscriber.request(double(:event, payload: event_3))
+
+ results = subject.results
+ expect(results[:calls]).to eq(3)
+ expect(results[:duration]).to eq("1335.00ms")
+ expect(results[:details].count).to eq(3)
+
+ expected = [
+ {
+ duration: 30.0,
+ label: "POST https://gitlab.com:80/api/v4/projects?current=true",
+ code: "Response status: 200",
+ proxy: nil,
+ error: nil,
+ warnings: []
+ },
+ {
+ duration: 1300,
+ label: "POST http://gitlab.com:80/api/v4/projects/2/issues?current=true",
+ code: nil,
+ proxy: nil,
+ error: "Exception: Net::ReadTimeout",
+ warnings: ["1300.0 over 100"]
+ },
+ {
+ duration: 5.0,
+ label: "GET http://gitlab.com:80/api/v4/projects/2?current=true",
+ code: "Response status: 301",
+ proxy: nil,
+ error: nil,
+ warnings: []
+ }
+ ]
+
+ expect(
+ results[:details].map { |data| data.slice(:duration, :label, :code, :proxy, :error, :warnings) }
+ ).to match_array(expected)
+ end
+
+ context 'when the host is in IPv4 format' do
+ before do
+ event_1[:host] = '1.2.3.4'
+ end
+
+ it 'displays IPv4 in the label' do
+ subscriber.request(double(:event, payload: event_1))
+
+ expect(subject.results[:details]).to contain_exactly(
+ a_hash_including(
+ duration: 30.0,
+ label: "POST https://1.2.3.4:80/api/v4/projects?current=true",
+ code: "Response status: 200",
+ proxy: nil,
+ error: nil,
+ warnings: []
+ )
+ )
+ end
+ end
+
+ context 'when the host is in IPv6 foramat' do
+ before do
+ event_1[:host] = '2606:4700:90:0:f22e:fbec:5bed:a9b9'
+ end
+
+ it 'displays IPv6 in the label' do
+ subscriber.request(double(:event, payload: event_1))
+
+ expect(subject.results[:details]).to contain_exactly(
+ a_hash_including(
+ duration: 30.0,
+ label: "POST https://[2606:4700:90:0:f22e:fbec:5bed:a9b9]:80/api/v4/projects?current=true",
+ code: "Response status: 200",
+ proxy: nil,
+ error: nil,
+ warnings: []
+ )
+ )
+ end
+ end
+
+ context 'when the query is a hash' do
+ before do
+ event_1[:query] = { current: true, 'item1' => 'string', 'item2' => [1, 2] }
+ end
+
+ it 'converts query hash into a query string' do
+ subscriber.request(double(:event, payload: event_1))
+
+ expect(subject.results[:details]).to contain_exactly(
+ a_hash_including(
+ duration: 30.0,
+ label: "POST https://gitlab.com:80/api/v4/projects?current=true&item1=string&item2%5B%5D=1&item2%5B%5D=2",
+ code: "Response status: 200",
+ proxy: nil,
+ error: nil,
+ warnings: []
+ )
+ )
+ end
+ end
+
+ context 'when the host is invalid' do
+ before do
+ event_1[:host] = '!@#%!@#%!@#%'
+ end
+
+ it 'displays unknown in the label' do
+ subscriber.request(double(:event, payload: event_1))
+
+ expect(subject.results[:details]).to contain_exactly(
+ a_hash_including(
+ duration: 30.0,
+ label: "POST unknown",
+ code: "Response status: 200",
+ proxy: nil,
+ error: nil,
+ warnings: []
+ )
+ )
+ end
+ end
+
+ context 'when URI creation raises an URI::Error' do
+ before do
+ # This raises an URI::Error exception
+ event_1[:port] = 'invalid'
+ end
+
+ it 'displays unknown in the label' do
+ subscriber.request(double(:event, payload: event_1))
+
+ expect(subject.results[:details]).to contain_exactly(
+ a_hash_including(
+ duration: 30.0,
+ label: "POST unknown",
+ code: "Response status: 200",
+ proxy: nil,
+ error: nil,
+ warnings: []
+ )
+ )
+ end
+ end
+
+ context 'when URI creation raises a StandardError exception' do
+ before do
+ # This raises a TypeError exception
+ event_1[:scheme] = 1234
+ end
+
+ it 'displays unknown in the label' do
+ subscriber.request(double(:event, payload: event_1))
+
+ expect(subject.results[:details]).to contain_exactly(
+ a_hash_including(
+ duration: 30.0,
+ label: "POST unknown",
+ code: "Response status: 200",
+ proxy: nil,
+ error: nil,
+ warnings: []
+ )
+ )
+ end
+ end
+end
diff --git a/spec/lib/release_highlights/validator/entry_spec.rb b/spec/lib/release_highlights/validator/entry_spec.rb
index 648356e63ba..da44938f165 100644
--- a/spec/lib/release_highlights/validator/entry_spec.rb
+++ b/spec/lib/release_highlights/validator/entry_spec.rb
@@ -80,7 +80,7 @@ RSpec.describe ReleaseHighlights::Validator::Entry do
subject.valid?
- expect(subject.errors[:packages].first).to include("must be one of", "Core", "Starter", "Premium", "Ultimate")
+ expect(subject.errors[:packages].first).to include("must be one of", "Free", "Premium", "Ultimate")
end
end
end
diff --git a/spec/lib/release_highlights/validator_spec.rb b/spec/lib/release_highlights/validator_spec.rb
index e68d9145dcd..a423e8cc5f6 100644
--- a/spec/lib/release_highlights/validator_spec.rb
+++ b/spec/lib/release_highlights/validator_spec.rb
@@ -70,7 +70,7 @@ RSpec.describe ReleaseHighlights::Validator do
---------------------------------------------------------
Validation failed for spec/fixtures/whats_new/invalid.yml
---------------------------------------------------------
- * Packages must be one of ["Core", "Starter", "Premium", "Ultimate"] (line 6)
+ * Packages must be one of ["Free", "Premium", "Ultimate"] (line 6)
MESSAGE
end
diff --git a/spec/lib/security/ci_configuration/sast_build_actions_spec.rb b/spec/lib/security/ci_configuration/sast_build_actions_spec.rb
new file mode 100644
index 00000000000..c8f9430eff9
--- /dev/null
+++ b/spec/lib/security/ci_configuration/sast_build_actions_spec.rb
@@ -0,0 +1,539 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Security::CiConfiguration::SastBuildActions do
+ let(:default_sast_values) do
+ { 'global' =>
+ [
+ { 'field' => 'SECURE_ANALYZERS_PREFIX', 'defaultValue' => 'registry.gitlab.com/gitlab-org/security-products/analyzers', 'value' => 'registry.gitlab.com/gitlab-org/security-products/analyzers' }
+ ],
+ 'pipeline' =>
+ [
+ { 'field' => 'stage', 'defaultValue' => 'test', 'value' => 'test' },
+ { 'field' => 'SEARCH_MAX_DEPTH', 'defaultValue' => 4, 'value' => 4 },
+ { 'field' => 'SAST_ANALYZER_IMAGE_TAG', 'defaultValue' => 2, 'value' => 2 },
+ { 'field' => 'SAST_EXCLUDED_PATHS', 'defaultValue' => 'spec, test, tests, tmp', 'value' => 'spec, test, tests, tmp' }
+ ] }
+ end
+
+ let(:params) do
+ { 'global' =>
+ [
+ { 'field' => 'SECURE_ANALYZERS_PREFIX', 'defaultValue' => 'registry.gitlab.com/gitlab-org/security-products/analyzers', 'value' => 'new_registry' }
+ ],
+ 'pipeline' =>
+ [
+ { 'field' => 'stage', 'defaultValue' => 'test', 'value' => 'security' },
+ { 'field' => 'SEARCH_MAX_DEPTH', 'defaultValue' => 4, 'value' => 1 },
+ { 'field' => 'SAST_ANALYZER_IMAGE_TAG', 'defaultValue' => 2, 'value' => 2 },
+ { 'field' => 'SAST_EXCLUDED_PATHS', 'defaultValue' => 'spec, test, tests, tmp', 'value' => 'spec,docs' }
+ ] }
+ end
+
+ let(:params_with_analyzer_info) do
+ params.merge( { 'analyzers' =>
+ [
+ {
+ 'name' => "bandit",
+ 'enabled' => false
+ },
+ {
+ 'name' => "brakeman",
+ 'enabled' => true,
+ 'variables' => [
+ { 'field' => "SAST_BRAKEMAN_LEVEL",
+ 'defaultValue' => "1",
+ 'value' => "2" }
+ ]
+ },
+ {
+ 'name' => "flawfinder",
+ 'enabled' => true,
+ 'variables' => [
+ { 'field' => "SAST_FLAWFINDER_LEVEL",
+ 'defaultValue' => "1",
+ 'value' => "1" }
+ ]
+ }
+ ] }
+ )
+ end
+
+ let(:params_with_all_analyzers_enabled) do
+ params.merge( { 'analyzers' =>
+ [
+ {
+ 'name' => "flawfinder",
+ 'enabled' => true
+ },
+ {
+ 'name' => "brakeman",
+ 'enabled' => true
+ }
+ ] }
+ )
+ end
+
+ context 'with existing .gitlab-ci.yml' do
+ let(:auto_devops_enabled) { false }
+
+ context 'sast has not been included' do
+ context 'template includes are array' do
+ let(:gitlab_ci_content) { existing_gitlab_ci_and_template_array_without_sast }
+
+ subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate }
+
+ it 'generates the correct YML' do
+ expect(result.first[:action]).to eq('update')
+ expect(result.first[:content]).to eq(sast_yaml_two_includes)
+ end
+ end
+
+ context 'template include is not an array' do
+ let(:gitlab_ci_content) { existing_gitlab_ci_and_single_template_without_sast }
+
+ subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate }
+
+ it 'generates the correct YML' do
+ expect(result.first[:action]).to eq('update')
+ expect(result.first[:content]).to eq(sast_yaml_two_includes)
+ end
+
+ it 'reports defaults have been overwritten' do
+ expect(result.first[:default_values_overwritten]).to eq(true)
+ end
+ end
+ end
+
+ context 'sast template include is not an array' do
+ let(:gitlab_ci_content) { existing_gitlab_ci_and_single_template_with_sast_and_default_stage }
+
+ subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate }
+
+ it 'generates the correct YML' do
+ expect(result.first[:action]).to eq('update')
+ expect(result.first[:content]).to eq(sast_yaml_all_params)
+ end
+ end
+
+ context 'with default values' do
+ let(:params) { default_sast_values }
+ let(:gitlab_ci_content) { existing_gitlab_ci_and_single_template_with_sast_and_default_stage }
+
+ subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate }
+
+ it 'generates the correct YML' do
+ expect(result.first[:content]).to eq(sast_yaml_with_no_variables_set)
+ end
+
+ it 'reports defaults have not been overwritten' do
+ expect(result.first[:default_values_overwritten]).to eq(false)
+ end
+
+ context 'analyzer section' do
+ let(:gitlab_ci_content) { existing_gitlab_ci_and_single_template_with_sast_and_default_stage }
+
+ subject(:result) { described_class.new(auto_devops_enabled, params_with_analyzer_info, gitlab_ci_content).generate }
+
+ it 'generates the correct YML' do
+ expect(result.first[:content]).to eq(sast_yaml_with_no_variables_set_but_analyzers)
+ end
+
+ context 'analyzers are disabled' do
+ let(:gitlab_ci_content) { existing_gitlab_ci_and_single_template_with_sast_and_default_stage }
+
+ subject(:result) { described_class.new(auto_devops_enabled, params_with_analyzer_info, gitlab_ci_content).generate }
+
+ it 'writes SAST_EXCLUDED_ANALYZERS' do
+ stub_const('Security::CiConfiguration::SastBuildActions::SAST_DEFAULT_ANALYZERS', 'bandit, brakeman, flawfinder')
+
+ expect(result.first[:content]).to eq(sast_yaml_with_no_variables_set_but_analyzers)
+ end
+ end
+
+ context 'all analyzers are enabled' do
+ let(:gitlab_ci_content) { existing_gitlab_ci_and_single_template_with_sast_and_default_stage }
+
+ subject(:result) { described_class.new(auto_devops_enabled, params_with_all_analyzers_enabled, gitlab_ci_content).generate }
+
+ it 'does not write SAST_DEFAULT_ANALYZERS or SAST_EXCLUDED_ANALYZERS' do
+ stub_const('Security::CiConfiguration::SastBuildActions::SAST_DEFAULT_ANALYZERS', 'brakeman, flawfinder')
+
+ expect(result.first[:content]).to eq(sast_yaml_with_no_variables_set)
+ end
+ end
+ end
+ end
+
+ context 'with update stage and SEARCH_MAX_DEPTH and set SECURE_ANALYZERS_PREFIX to default' do
+ let(:params) do
+ { 'global' =>
+ [
+ { 'field' => 'SECURE_ANALYZERS_PREFIX', 'defaultValue' => 'registry.gitlab.com/gitlab-org/security-products/analyzers', 'value' => 'registry.gitlab.com/gitlab-org/security-products/analyzers' }
+ ],
+ 'pipeline' =>
+ [
+ { 'field' => 'stage', 'defaultValue' => 'test', 'value' => 'brand_new_stage' },
+ { 'field' => 'SEARCH_MAX_DEPTH', 'defaultValue' => 4, 'value' => 5 },
+ { 'field' => 'SAST_ANALYZER_IMAGE_TAG', 'defaultValue' => 2, 'value' => 2 },
+ { 'field' => 'SAST_EXCLUDED_PATHS', 'defaultValue' => 'spec, test, tests, tmp', 'value' => 'spec,docs' }
+ ] }
+ end
+
+ let(:gitlab_ci_content) { existing_gitlab_ci }
+
+ subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate }
+
+ it 'generates the correct YML' do
+ expect(result.first[:action]).to eq('update')
+ expect(result.first[:content]).to eq(sast_yaml_updated_stage)
+ end
+ end
+
+ context 'with no existing variables' do
+ let(:gitlab_ci_content) { existing_gitlab_ci_with_no_variables }
+
+ subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate }
+
+ it 'generates the correct YML' do
+ expect(result.first[:action]).to eq('update')
+ expect(result.first[:content]).to eq(sast_yaml_variable_section_added)
+ end
+ end
+
+ context 'with no existing sast config' do
+ let(:gitlab_ci_content) { existing_gitlab_ci_with_no_sast_section }
+
+ subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate }
+
+ it 'generates the correct YML' do
+ expect(result.first[:action]).to eq('update')
+ expect(result.first[:content]).to eq(sast_yaml_sast_section_added)
+ end
+ end
+
+ context 'with no existing sast variables' do
+ let(:gitlab_ci_content) { existing_gitlab_ci_with_no_sast_variables }
+
+ subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate }
+
+ it 'generates the correct YML' do
+ expect(result.first[:action]).to eq('update')
+ expect(result.first[:content]).to eq(sast_yaml_sast_variables_section_added)
+ end
+ end
+
+ def existing_gitlab_ci_and_template_array_without_sast
+ { "stages" => %w(test security),
+ "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" },
+ "sast" => { "variables" => { "SAST_ANALYZER_IMAGE_TAG" => 2, "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" },
+ "include" => [{ "template" => "existing.yml" }] }
+ end
+
+ def existing_gitlab_ci_and_single_template_with_sast_and_default_stage
+ { "stages" => %w(test),
+ "variables" => { "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" },
+ "sast" => { "variables" => { "SAST_ANALYZER_IMAGE_TAG" => 2, "SEARCH_MAX_DEPTH" => 1 }, "stage" => "test" },
+ "include" => { "template" => "Security/SAST.gitlab-ci.yml" } }
+ end
+
+ def existing_gitlab_ci_and_single_template_without_sast
+ { "stages" => %w(test security),
+ "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" },
+ "sast" => { "variables" => { "SAST_ANALYZER_IMAGE_TAG" => 2, "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" },
+ "include" => { "template" => "existing.yml" } }
+ end
+
+ def existing_gitlab_ci_with_no_variables
+ { "stages" => %w(test security),
+ "sast" => { "variables" => { "SAST_ANALYZER_IMAGE_TAG" => 2, "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" },
+ "include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] }
+ end
+
+ def existing_gitlab_ci_with_no_sast_section
+ { "stages" => %w(test security),
+ "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" },
+ "include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] }
+ end
+
+ def existing_gitlab_ci_with_no_sast_variables
+ { "stages" => %w(test security),
+ "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" },
+ "sast" => { "stage" => "security" },
+ "include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] }
+ end
+
+ def existing_gitlab_ci
+ { "stages" => %w(test security),
+ "variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "bad_prefix" },
+ "sast" => { "variables" => { "SAST_ANALYZER_IMAGE_TAG" => 2, "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" },
+ "include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] }
+ end
+ end
+
+ context 'with no .gitlab-ci.yml' do
+ let(:gitlab_ci_content) { nil }
+
+ context 'autodevops disabled' do
+ let(:auto_devops_enabled) { false }
+
+ context 'with one empty parameter' do
+ let(:params) do
+ { 'global' =>
+ [
+ { 'field' => 'SECURE_ANALYZERS_PREFIX', 'defaultValue' => 'registry.gitlab.com/gitlab-org/security-products/analyzers', 'value' => '' }
+ ] }
+ end
+
+ subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate }
+
+ it 'generates the correct YML' do
+ expect(result.first[:content]).to eq(sast_yaml_with_no_variables_set)
+ end
+ end
+
+ context 'with all parameters' do
+ subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate }
+
+ it 'generates the correct YML' do
+ expect(result.first[:content]).to eq(sast_yaml_all_params)
+ end
+ end
+ end
+
+ context 'with autodevops enabled' do
+ let(:auto_devops_enabled) { true }
+
+ subject(:result) { described_class.new(auto_devops_enabled, params, gitlab_ci_content).generate }
+
+ before do
+ allow_next_instance_of(described_class) do |sast_build_actions|
+ allow(sast_build_actions).to receive(:auto_devops_stages).and_return(fast_auto_devops_stages)
+ end
+ end
+
+ it 'generates the correct YML' do
+ expect(result.first[:content]).to eq(auto_devops_with_custom_stage)
+ end
+ end
+ end
+
+ describe 'Security::CiConfiguration::SastBuildActions::SAST_DEFAULT_ANALYZERS' do
+ subject(:variable) {Security::CiConfiguration::SastBuildActions::SAST_DEFAULT_ANALYZERS}
+
+ it 'is sorted alphabetically' do
+ sorted_variable = Security::CiConfiguration::SastBuildActions::SAST_DEFAULT_ANALYZERS
+ .split(',')
+ .map(&:strip)
+ .sort
+ .join(', ')
+
+ expect(variable).to eq(sorted_variable)
+ end
+ end
+
+ # stubbing this method allows this spec file to use fast_spec_helper
+ def fast_auto_devops_stages
+ auto_devops_template = YAML.safe_load( File.read('lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml') )
+ auto_devops_template['stages']
+ end
+
+ def sast_yaml_with_no_variables_set_but_analyzers
+ <<-CI_YML.strip_heredoc
+ # You can override the included template(s) by including variable overrides
+ # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
+ # Note that environment variables can be set in several places
+ # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables
+ stages:
+ - test
+ sast:
+ variables:
+ SAST_EXCLUDED_ANALYZERS: bandit
+ SAST_BRAKEMAN_LEVEL: '2'
+ stage: test
+ include:
+ - template: Security/SAST.gitlab-ci.yml
+ CI_YML
+ end
+
+ def sast_yaml_with_no_variables_set
+ <<-CI_YML.strip_heredoc
+ # You can override the included template(s) by including variable overrides
+ # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
+ # Note that environment variables can be set in several places
+ # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables
+ stages:
+ - test
+ sast:
+ stage: test
+ include:
+ - template: Security/SAST.gitlab-ci.yml
+ CI_YML
+ end
+
+ def sast_yaml_all_params
+ <<-CI_YML.strip_heredoc
+ # You can override the included template(s) by including variable overrides
+ # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
+ # Note that environment variables can be set in several places
+ # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables
+ stages:
+ - test
+ - security
+ variables:
+ SECURE_ANALYZERS_PREFIX: new_registry
+ sast:
+ variables:
+ SAST_EXCLUDED_PATHS: spec,docs
+ SEARCH_MAX_DEPTH: 1
+ stage: security
+ include:
+ - template: Security/SAST.gitlab-ci.yml
+ CI_YML
+ end
+
+ def auto_devops_with_custom_stage
+ <<-CI_YML.strip_heredoc
+ # You can override the included template(s) by including variable overrides
+ # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
+ # Note that environment variables can be set in several places
+ # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables
+ stages:
+ - build
+ - test
+ - deploy
+ - review
+ - dast
+ - staging
+ - canary
+ - production
+ - incremental rollout 10%
+ - incremental rollout 25%
+ - incremental rollout 50%
+ - incremental rollout 100%
+ - performance
+ - cleanup
+ - security
+ variables:
+ SECURE_ANALYZERS_PREFIX: new_registry
+ sast:
+ variables:
+ SAST_EXCLUDED_PATHS: spec,docs
+ SEARCH_MAX_DEPTH: 1
+ stage: security
+ include:
+ - template: Auto-DevOps.gitlab-ci.yml
+ CI_YML
+ end
+
+ def sast_yaml_two_includes
+ <<-CI_YML.strip_heredoc
+ # You can override the included template(s) by including variable overrides
+ # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
+ # Note that environment variables can be set in several places
+ # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables
+ stages:
+ - test
+ - security
+ variables:
+ RANDOM: make sure this persists
+ SECURE_ANALYZERS_PREFIX: new_registry
+ sast:
+ variables:
+ SAST_EXCLUDED_PATHS: spec,docs
+ SEARCH_MAX_DEPTH: 1
+ stage: security
+ include:
+ - template: existing.yml
+ - template: Security/SAST.gitlab-ci.yml
+ CI_YML
+ end
+
+ def sast_yaml_variable_section_added
+ <<-CI_YML.strip_heredoc
+ # You can override the included template(s) by including variable overrides
+ # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
+ # Note that environment variables can be set in several places
+ # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables
+ stages:
+ - test
+ - security
+ sast:
+ variables:
+ SAST_EXCLUDED_PATHS: spec,docs
+ SEARCH_MAX_DEPTH: 1
+ stage: security
+ include:
+ - template: Security/SAST.gitlab-ci.yml
+ variables:
+ SECURE_ANALYZERS_PREFIX: new_registry
+ CI_YML
+ end
+
+ def sast_yaml_sast_section_added
+ <<-CI_YML.strip_heredoc
+ # You can override the included template(s) by including variable overrides
+ # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
+ # Note that environment variables can be set in several places
+ # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables
+ stages:
+ - test
+ - security
+ variables:
+ RANDOM: make sure this persists
+ SECURE_ANALYZERS_PREFIX: new_registry
+ include:
+ - template: Security/SAST.gitlab-ci.yml
+ sast:
+ variables:
+ SAST_EXCLUDED_PATHS: spec,docs
+ SEARCH_MAX_DEPTH: 1
+ stage: security
+ CI_YML
+ end
+
+ def sast_yaml_sast_variables_section_added
+ <<-CI_YML.strip_heredoc
+ # You can override the included template(s) by including variable overrides
+ # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
+ # Note that environment variables can be set in several places
+ # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables
+ stages:
+ - test
+ - security
+ variables:
+ RANDOM: make sure this persists
+ SECURE_ANALYZERS_PREFIX: new_registry
+ sast:
+ stage: security
+ variables:
+ SAST_EXCLUDED_PATHS: spec,docs
+ SEARCH_MAX_DEPTH: 1
+ include:
+ - template: Security/SAST.gitlab-ci.yml
+ CI_YML
+ end
+
+ def sast_yaml_updated_stage
+ <<-CI_YML.strip_heredoc
+ # You can override the included template(s) by including variable overrides
+ # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
+ # Note that environment variables can be set in several places
+ # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables
+ stages:
+ - test
+ - security
+ - brand_new_stage
+ variables:
+ RANDOM: make sure this persists
+ sast:
+ variables:
+ SAST_EXCLUDED_PATHS: spec,docs
+ SEARCH_MAX_DEPTH: 5
+ stage: brand_new_stage
+ include:
+ - template: Security/SAST.gitlab-ci.yml
+ CI_YML
+ end
+end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 53ce200eed5..89cf1aaedd2 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -905,6 +905,28 @@ RSpec.describe Notify do
is_expected.to have_body_text project.full_name
is_expected.to have_body_text project_member.human_access.downcase
is_expected.to have_body_text project_member.invite_token
+ is_expected.to have_link('Join now', href: invite_url(project_member.invite_token, invite_type: Members::InviteEmailExperiment::INVITE_TYPE))
+ end
+
+ it 'contains invite link for the avatar', :experiment do
+ stub_experiments('members/invite_email': :avatar)
+
+ is_expected.not_to have_content('You are invited!')
+ is_expected.not_to have_body_text 'What is a GitLab'
+ end
+
+ it 'contains invite link for the avatar', :experiment do
+ stub_experiments('members/invite_email': :permission_info)
+
+ is_expected.not_to have_content('You are invited!')
+ is_expected.to have_body_text 'What is a GitLab'
+ is_expected.to have_body_text 'What can I do with'
+ end
+
+ it 'has invite link for the control group' do
+ stub_experiments('members/invite_email': :control)
+
+ is_expected.to have_content('You are invited!')
end
end
diff --git a/spec/migrations/20201112130710_schedule_remove_duplicate_vulnerabilities_findings_spec.rb b/spec/migrations/20201112130710_schedule_remove_duplicate_vulnerabilities_findings_spec.rb
new file mode 100644
index 00000000000..ff27bdcf12d
--- /dev/null
+++ b/spec/migrations/20201112130710_schedule_remove_duplicate_vulnerabilities_findings_spec.rb
@@ -0,0 +1,140 @@
+# frozen_string_literal: true
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20201112130710_schedule_remove_duplicate_vulnerabilities_findings.rb')
+
+RSpec.describe ScheduleRemoveDuplicateVulnerabilitiesFindings, :migration do
+ let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+ let(:users) { table(:users) }
+ let(:user) { create_user! }
+ let(:project) { table(:projects).create!(id: 123, namespace_id: namespace.id) }
+ let(:scanners) { table(:vulnerability_scanners) }
+ let!(:scanner) { scanners.create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
+ let!(:scanner2) { scanners.create!(project_id: project.id, external_id: 'test 2', name: 'test scanner 2') }
+ let!(:scanner3) { scanners.create!(project_id: project.id, external_id: 'test 3', name: 'test scanner 3') }
+ let!(:unrelated_scanner) { scanners.create!(project_id: project.id, external_id: 'unreleated_scanner', name: 'unrelated scanner') }
+ let(:vulnerabilities) { table(:vulnerabilities) }
+ let(:vulnerability_findings) { table(:vulnerability_occurrences) }
+ let(:vulnerability_identifiers) { table(:vulnerability_identifiers) }
+ let(:vulnerability_identifier) do
+ vulnerability_identifiers.create!(
+ project_id: project.id,
+ external_type: 'vulnerability-identifier',
+ external_id: 'vulnerability-identifier',
+ fingerprint: '7e394d1b1eb461a7406d7b1e08f057a1cf11287a',
+ name: 'vulnerability identifier')
+ end
+
+ let!(:first_finding) do
+ create_finding!(
+ uuid: "test1",
+ vulnerability_id: nil,
+ report_type: 0,
+ location_fingerprint: '2bda3014914481791847d8eca38d1a8d13b6ad76',
+ primary_identifier_id: vulnerability_identifier.id,
+ scanner_id: scanner.id,
+ project_id: project.id
+ )
+ end
+
+ let!(:first_duplicate) do
+ create_finding!(
+ uuid: "test2",
+ vulnerability_id: nil,
+ report_type: 0,
+ location_fingerprint: '2bda3014914481791847d8eca38d1a8d13b6ad76',
+ primary_identifier_id: vulnerability_identifier.id,
+ scanner_id: scanner2.id,
+ project_id: project.id
+ )
+ end
+
+ let!(:second_duplicate) do
+ create_finding!(
+ uuid: "test3",
+ vulnerability_id: nil,
+ report_type: 0,
+ location_fingerprint: '2bda3014914481791847d8eca38d1a8d13b6ad76',
+ primary_identifier_id: vulnerability_identifier.id,
+ scanner_id: scanner3.id,
+ project_id: project.id
+ )
+ end
+
+ let!(:unrelated_finding) do
+ create_finding!(
+ uuid: "unreleated_finding",
+ vulnerability_id: nil,
+ report_type: 1,
+ location_fingerprint: 'random_location_fingerprint',
+ primary_identifier_id: vulnerability_identifier.id,
+ scanner_id: unrelated_scanner.id,
+ project_id: project.id
+ )
+ end
+
+ before do
+ stub_const("#{described_class}::BATCH_SIZE", 1)
+ end
+
+ around do |example|
+ freeze_time { Sidekiq::Testing.fake! { example.run } }
+ end
+
+ it 'schedules background migration' do
+ migrate!
+
+ expect(BackgroundMigrationWorker.jobs.size).to eq(4)
+ expect(described_class::MIGRATION).to be_scheduled_migration(first_finding.id, first_finding.id)
+ expect(described_class::MIGRATION).to be_scheduled_migration(first_duplicate.id, first_duplicate.id)
+ expect(described_class::MIGRATION).to be_scheduled_migration(second_duplicate.id, second_duplicate.id)
+ expect(described_class::MIGRATION).to be_scheduled_migration(unrelated_finding.id, unrelated_finding.id)
+ end
+
+ private
+
+ def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0)
+ vulnerabilities.create!(
+ project_id: project_id,
+ author_id: author_id,
+ title: title,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type
+ )
+ end
+
+ # rubocop:disable Metrics/ParameterLists
+ def create_finding!(
+ vulnerability_id:, project_id:, scanner_id:, primary_identifier_id:,
+ name: "test", severity: 7, confidence: 7, report_type: 0,
+ project_fingerprint: '123qweasdzxc', location_fingerprint: 'test',
+ metadata_version: 'test', raw_metadata: 'test', uuid: 'test')
+ vulnerability_findings.create!(
+ vulnerability_id: vulnerability_id,
+ project_id: project_id,
+ name: name,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type,
+ project_fingerprint: project_fingerprint,
+ scanner_id: scanner_id,
+ primary_identifier_id: vulnerability_identifier.id,
+ location_fingerprint: location_fingerprint,
+ metadata_version: metadata_version,
+ raw_metadata: raw_metadata,
+ uuid: uuid
+ )
+ end
+ # rubocop:enable Metrics/ParameterLists
+
+ def create_user!(name: "Example User", email: "user@example.com", user_type: nil, created_at: Time.now, confirmed_at: Time.now)
+ users.create!(
+ name: name,
+ email: email,
+ username: name,
+ projects_limit: 0,
+ user_type: user_type,
+ confirmed_at: confirmed_at
+ )
+ end
+end
diff --git a/spec/migrations/20210119122354_alter_vsa_issue_first_mentioned_in_commit_value_spec.rb b/spec/migrations/20210119122354_alter_vsa_issue_first_mentioned_in_commit_value_spec.rb
new file mode 100644
index 00000000000..33b2e009c8d
--- /dev/null
+++ b/spec/migrations/20210119122354_alter_vsa_issue_first_mentioned_in_commit_value_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20210119122354_alter_vsa_issue_first_mentioned_in_commit_value.rb')
+
+RSpec.describe AlterVsaIssueFirstMentionedInCommitValue, schema: 20210114033715 do
+ let(:group_stages) { table(:analytics_cycle_analytics_group_stages) }
+ let(:value_streams) { table(:analytics_cycle_analytics_group_value_streams) }
+ let(:namespaces) { table(:namespaces) }
+
+ let(:namespace) { namespaces.create!(id: 1, name: 'group', path: 'group') }
+ let(:value_stream) { value_streams.create!(name: 'test', group_id: namespace.id) }
+
+ let!(:stage_1) { group_stages.create!(group_value_stream_id: value_stream.id, group_id: namespace.id, name: 'stage 1', start_event_identifier: described_class::ISSUE_FIRST_MENTIONED_IN_COMMIT_EE, end_event_identifier: 1) }
+ let!(:stage_2) { group_stages.create!(group_value_stream_id: value_stream.id, group_id: namespace.id, name: 'stage 2', start_event_identifier: 2, end_event_identifier: described_class::ISSUE_FIRST_MENTIONED_IN_COMMIT_EE) }
+ let!(:stage_3) { group_stages.create!(group_value_stream_id: value_stream.id, group_id: namespace.id, name: 'stage 3', start_event_identifier: described_class::ISSUE_FIRST_MENTIONED_IN_COMMIT_FOSS, end_event_identifier: 3) }
+
+ describe '#up' do
+ it 'changes the EE specific identifier values to the FOSS version' do
+ migrate!
+
+ expect(stage_1.reload.start_event_identifier).to eq(described_class::ISSUE_FIRST_MENTIONED_IN_COMMIT_FOSS)
+ expect(stage_2.reload.end_event_identifier).to eq(described_class::ISSUE_FIRST_MENTIONED_IN_COMMIT_FOSS)
+ end
+
+ it 'does not change irrelevant records' do
+ expect { migrate! }.not_to change { stage_3.reload }
+ end
+ end
+end
diff --git a/spec/migrations/20210205174154_remove_bad_dependency_proxy_manifests_spec.rb b/spec/migrations/20210205174154_remove_bad_dependency_proxy_manifests_spec.rb
new file mode 100644
index 00000000000..7e351617ea3
--- /dev/null
+++ b/spec/migrations/20210205174154_remove_bad_dependency_proxy_manifests_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20210205174154_remove_bad_dependency_proxy_manifests.rb')
+
+RSpec.describe RemoveBadDependencyProxyManifests, schema: 20210128140157 do
+ let_it_be(:namespaces) { table(:namespaces) }
+ let_it_be(:dependency_proxy_manifests) { table(:dependency_proxy_manifests) }
+ let_it_be(:group) { namespaces.create!(type: 'Group', name: 'test', path: 'test') }
+
+ let_it_be(:dependency_proxy_manifest_with_content_type) do
+ dependency_proxy_manifests.create!(group_id: group.id, file: 'foo', file_name: 'foo', digest: 'asdf1234', content_type: 'content-type' )
+ end
+
+ let_it_be(:dependency_proxy_manifest_without_content_type) do
+ dependency_proxy_manifests.create!(group_id: group.id, file: 'bar', file_name: 'bar', digest: 'fdsa6789')
+ end
+
+ it 'removes the dependency_proxy_manifests with a content_type', :aggregate_failures do
+ expect(dependency_proxy_manifest_with_content_type).to be_present
+ expect(dependency_proxy_manifest_without_content_type).to be_present
+
+ expect { migrate! }.to change { dependency_proxy_manifests.count }.from(2).to(1)
+
+ expect(dependency_proxy_manifests.where.not(content_type: nil)).to be_empty
+ expect(dependency_proxy_manifest_without_content_type.reload).to be_present
+ end
+end
diff --git a/spec/migrations/20210210093901_backfill_updated_at_after_repository_storage_move_spec.rb b/spec/migrations/20210210093901_backfill_updated_at_after_repository_storage_move_spec.rb
new file mode 100644
index 00000000000..52678111b48
--- /dev/null
+++ b/spec/migrations/20210210093901_backfill_updated_at_after_repository_storage_move_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20210210093901_backfill_updated_at_after_repository_storage_move.rb')
+
+RSpec.describe BackfillUpdatedAtAfterRepositoryStorageMove, :sidekiq do
+ let_it_be(:projects) { table(:projects) }
+ let_it_be(:project_repository_storage_moves) { table(:project_repository_storage_moves) }
+ let_it_be(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+
+ describe '#up' do
+ it 'schedules background jobs for all distinct projects in batches' do
+ stub_const("#{described_class}::BATCH_SIZE", 3)
+
+ project_1 = projects.create!(id: 1, namespace_id: namespace.id)
+ project_2 = projects.create!(id: 2, namespace_id: namespace.id)
+ project_3 = projects.create!(id: 3, namespace_id: namespace.id)
+ project_4 = projects.create!(id: 4, namespace_id: namespace.id)
+ project_5 = projects.create!(id: 5, namespace_id: namespace.id)
+ project_6 = projects.create!(id: 6, namespace_id: namespace.id)
+ project_7 = projects.create!(id: 7, namespace_id: namespace.id)
+ projects.create!(id: 8, namespace_id: namespace.id)
+
+ project_repository_storage_moves.create!(id: 1, project_id: project_1.id, source_storage_name: 'default', destination_storage_name: 'default')
+ project_repository_storage_moves.create!(id: 2, project_id: project_1.id, source_storage_name: 'default', destination_storage_name: 'default')
+ project_repository_storage_moves.create!(id: 3, project_id: project_2.id, source_storage_name: 'default', destination_storage_name: 'default')
+ project_repository_storage_moves.create!(id: 4, project_id: project_3.id, source_storage_name: 'default', destination_storage_name: 'default')
+ project_repository_storage_moves.create!(id: 5, project_id: project_3.id, source_storage_name: 'default', destination_storage_name: 'default')
+ project_repository_storage_moves.create!(id: 6, project_id: project_4.id, source_storage_name: 'default', destination_storage_name: 'default')
+ project_repository_storage_moves.create!(id: 7, project_id: project_4.id, source_storage_name: 'default', destination_storage_name: 'default')
+ project_repository_storage_moves.create!(id: 8, project_id: project_5.id, source_storage_name: 'default', destination_storage_name: 'default')
+ project_repository_storage_moves.create!(id: 9, project_id: project_6.id, source_storage_name: 'default', destination_storage_name: 'default')
+ project_repository_storage_moves.create!(id: 10, project_id: project_7.id, source_storage_name: 'default', destination_storage_name: 'default')
+
+ Sidekiq::Testing.fake! do
+ freeze_time do
+ migrate!
+
+ expect(BackgroundMigrationWorker.jobs.size).to eq(3)
+ expect(described_class::MIGRATION_CLASS).to be_scheduled_delayed_migration(2.minutes, 1, 2, 3)
+ expect(described_class::MIGRATION_CLASS).to be_scheduled_delayed_migration(4.minutes, 4, 5, 6)
+ expect(described_class::MIGRATION_CLASS).to be_scheduled_delayed_migration(6.minutes, 7)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/migrations/add_has_external_issue_tracker_trigger_spec.rb b/spec/migrations/add_has_external_issue_tracker_trigger_spec.rb
new file mode 100644
index 00000000000..72983c7cfbf
--- /dev/null
+++ b/spec/migrations/add_has_external_issue_tracker_trigger_spec.rb
@@ -0,0 +1,164 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe AddHasExternalIssueTrackerTrigger do
+ let(:migration) { described_class.new }
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:services) { table(:services) }
+
+ before do
+ @namespace = namespaces.create!(name: 'foo', path: 'foo')
+ @project = projects.create!(namespace_id: @namespace.id)
+ end
+
+ describe '#up' do
+ before do
+ migrate!
+ end
+
+ describe 'INSERT trigger' do
+ it 'sets `has_external_issue_tracker` to true when active `issue_tracker` is inserted' do
+ expect do
+ services.create!(category: 'issue_tracker', active: true, project_id: @project.id)
+ end.to change { @project.reload.has_external_issue_tracker }.to(true)
+ end
+
+ it 'does not set `has_external_issue_tracker` to true when service is for a different project' do
+ different_project = projects.create!(namespace_id: @namespace.id)
+
+ expect do
+ services.create!(category: 'issue_tracker', active: true, project_id: different_project.id)
+ end.not_to change { @project.reload.has_external_issue_tracker }
+ end
+
+ it 'does not set `has_external_issue_tracker` to true when inactive `issue_tracker` is inserted' do
+ expect do
+ services.create!(category: 'issue_tracker', active: false, project_id: @project.id)
+ end.not_to change { @project.reload.has_external_issue_tracker }
+ end
+
+ it 'does not set `has_external_issue_tracker` to true when a non-`issue tracker` active service is inserted' do
+ expect do
+ services.create!(category: 'my_type', active: true, project_id: @project.id)
+ end.not_to change { @project.reload.has_external_issue_tracker }
+ end
+ end
+
+ describe 'UPDATE trigger' do
+ it 'sets `has_external_issue_tracker` to true when `issue_tracker` is made active' do
+ service = services.create!(category: 'issue_tracker', active: false, project_id: @project.id)
+
+ expect do
+ service.update!(active: true)
+ end.to change { @project.reload.has_external_issue_tracker }.to(true)
+ end
+
+ it 'sets `has_external_issue_tracker` to false when `issue_tracker` is made inactive' do
+ service = services.create!(category: 'issue_tracker', active: true, project_id: @project.id)
+
+ expect do
+ service.update!(active: false)
+ end.to change { @project.reload.has_external_issue_tracker }.to(false)
+ end
+
+ it 'sets `has_external_issue_tracker` to false when `issue_tracker` is made inactive, and an inactive `issue_tracker` exists' do
+ services.create!(category: 'issue_tracker', active: false, project_id: @project.id)
+ service = services.create!(category: 'issue_tracker', active: true, project_id: @project.id)
+
+ expect do
+ service.update!(active: false)
+ end.to change { @project.reload.has_external_issue_tracker }.to(false)
+ end
+
+ it 'does not change `has_external_issue_tracker` when `issue_tracker` is made inactive, if an active `issue_tracker` exists' do
+ services.create!(category: 'issue_tracker', active: true, project_id: @project.id)
+ service = services.create!(category: 'issue_tracker', active: true, project_id: @project.id)
+
+ expect do
+ service.update!(active: false)
+ end.not_to change { @project.reload.has_external_issue_tracker }
+ end
+
+ it 'does not change `has_external_issue_tracker` when service is for a different project' do
+ different_project = projects.create!(namespace_id: @namespace.id)
+ service = services.create!(category: 'issue_tracker', active: false, project_id: different_project.id)
+
+ expect do
+ service.update!(active: true)
+ end.not_to change { @project.reload.has_external_issue_tracker }
+ end
+ end
+
+ describe 'DELETE trigger' do
+ it 'sets `has_external_issue_tracker` to false when `issue_tracker` is deleted' do
+ service = services.create!(category: 'issue_tracker', active: true, project_id: @project.id)
+
+ expect do
+ service.delete
+ end.to change { @project.reload.has_external_issue_tracker }.to(false)
+ end
+
+ it 'sets `has_external_issue_tracker` to false when `issue_tracker` is deleted, if an inactive `issue_tracker` still exists' do
+ services.create!(category: 'issue_tracker', active: false, project_id: @project.id)
+ service = services.create!(category: 'issue_tracker', active: true, project_id: @project.id)
+
+ expect do
+ service.delete
+ end.to change { @project.reload.has_external_issue_tracker }.to(false)
+ end
+
+ it 'does not change `has_external_issue_tracker` when `issue_tracker` is deleted, if an active `issue_tracker` still exists' do
+ services.create!(category: 'issue_tracker', active: true, project_id: @project.id)
+ service = services.create!(category: 'issue_tracker', active: true, project_id: @project.id)
+
+ expect do
+ service.delete
+ end.not_to change { @project.reload.has_external_issue_tracker }
+ end
+
+ it 'does not change `has_external_issue_tracker` when service is for a different project' do
+ different_project = projects.create!(namespace_id: @namespace.id)
+ service = services.create!(category: 'issue_tracker', active: true, project_id: different_project.id)
+
+ expect do
+ service.delete
+ end.not_to change { @project.reload.has_external_issue_tracker }
+ end
+ end
+ end
+
+ describe '#down' do
+ before do
+ migration.up
+ migration.down
+ end
+
+ it 'drops the INSERT trigger' do
+ expect do
+ services.create!(category: 'issue_tracker', active: true, project_id: @project.id)
+ end.not_to change { @project.reload.has_external_issue_tracker }
+ end
+
+ it 'drops the UPDATE trigger' do
+ service = services.create!(category: 'issue_tracker', active: false, project_id: @project.id)
+ @project.update!(has_external_issue_tracker: false)
+
+ expect do
+ service.update!(active: true)
+ end.not_to change { @project.reload.has_external_issue_tracker }
+ end
+
+ it 'drops the DELETE trigger' do
+ service = services.create!(category: 'issue_tracker', active: true, project_id: @project.id)
+ @project.update!(has_external_issue_tracker: true)
+
+ expect do
+ service.delete
+ end.not_to change { @project.reload.has_external_issue_tracker }
+ end
+ end
+end
diff --git a/spec/migrations/add_new_post_eoa_plans_spec.rb b/spec/migrations/add_new_post_eoa_plans_spec.rb
new file mode 100644
index 00000000000..5ae227a6617
--- /dev/null
+++ b/spec/migrations/add_new_post_eoa_plans_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require Rails.root.join('db', 'post_migrate', '20210205104425_add_new_post_eoa_plans.rb')
+
+RSpec.describe AddNewPostEoaPlans do
+ let(:plans) { table(:plans) }
+
+ subject(:migration) { described_class.new }
+
+ describe '#up' do
+ it 'creates the two new records' do
+ expect { migration.up }.to change { plans.count }.by(2)
+
+ new_plans = plans.last(2)
+ expect(new_plans.map(&:name)).to contain_exactly('premium', 'ultimate')
+ end
+ end
+
+ describe '#down' do
+ it 'removes these two new records' do
+ plans.create!(name: 'premium', title: 'Premium (Formerly Silver)')
+ plans.create!(name: 'ultimate', title: 'Ultimate (Formerly Gold)')
+
+ expect { migration.down }.to change { plans.count }.by(-2)
+
+ expect(plans.find_by(name: 'premium')).to be_nil
+ expect(plans.find_by(name: 'ultimate')).to be_nil
+ end
+ end
+end
diff --git a/spec/migrations/cleanup_projects_with_bad_has_external_wiki_data_spec.rb b/spec/migrations/cleanup_projects_with_bad_has_external_wiki_data_spec.rb
new file mode 100644
index 00000000000..ee1f718849f
--- /dev/null
+++ b/spec/migrations/cleanup_projects_with_bad_has_external_wiki_data_spec.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe CleanupProjectsWithBadHasExternalWikiData, :migration do
+ let(:namespace) { table(:namespaces).create!(name: 'foo', path: 'bar') }
+ let(:projects) { table(:projects) }
+ let(:services) { table(:services) }
+
+ def create_projects!(num)
+ Array.new(num) do
+ projects.create!(namespace_id: namespace.id)
+ end
+ end
+
+ def create_active_external_wiki_integrations!(*projects)
+ projects.each do |project|
+ services.create!(type: 'ExternalWikiService', project_id: project.id, active: true)
+ end
+ end
+
+ def create_disabled_external_wiki_integrations!(*projects)
+ projects.each do |project|
+ services.create!(type: 'ExternalWikiService', project_id: project.id, active: false)
+ end
+ end
+
+ def create_active_other_integrations!(*projects)
+ projects.each do |project|
+ services.create!(type: 'NotAnExternalWikiService', project_id: project.id, active: true)
+ end
+ end
+
+ it 'sets `projects.has_external_wiki` correctly' do
+ allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
+
+ project_with_external_wiki_1,
+ project_with_external_wiki_2,
+ project_with_disabled_external_wiki_1,
+ project_with_disabled_external_wiki_2,
+ project_without_external_wiki_1,
+ project_without_external_wiki_2 = create_projects!(6)
+
+ create_active_external_wiki_integrations!(
+ project_with_external_wiki_1,
+ project_with_external_wiki_2
+ )
+
+ create_disabled_external_wiki_integrations!(
+ project_with_disabled_external_wiki_1,
+ project_with_disabled_external_wiki_2
+ )
+
+ create_active_other_integrations!(
+ project_without_external_wiki_1,
+ project_without_external_wiki_2
+ )
+
+ # PG triggers on the services table added in a previous migration
+ # will have set the `has_external_wiki` columns to correct data when
+ # the services records were created above.
+ #
+ # We set the `has_external_wiki` columns for projects to incorrect
+ # data manually below to emulate projects in a state before the PG
+ # triggers were added.
+ project_with_external_wiki_2.update!(has_external_wiki: false)
+ project_with_disabled_external_wiki_2.update!(has_external_wiki: true)
+ project_without_external_wiki_2.update!(has_external_wiki: true)
+
+ migrate!
+
+ expected_true = [
+ project_with_external_wiki_1,
+ project_with_external_wiki_2
+ ].each(&:reload).map(&:has_external_wiki)
+
+ expected_not_true = [
+ project_without_external_wiki_1,
+ project_without_external_wiki_2,
+ project_with_disabled_external_wiki_1,
+ project_with_disabled_external_wiki_2
+ ].each(&:reload).map(&:has_external_wiki)
+
+ expect(expected_true).to all(eq(true))
+ expect(expected_not_true).to all(be_falsey)
+ end
+end
diff --git a/spec/migrations/drop_alerts_service_data_spec.rb b/spec/migrations/drop_alerts_service_data_spec.rb
new file mode 100644
index 00000000000..b2128da938f
--- /dev/null
+++ b/spec/migrations/drop_alerts_service_data_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20210205213933_drop_alerts_service_data.rb')
+
+RSpec.describe DropAlertsServiceData do
+ let_it_be(:alerts_service_data) { table(:alerts_service_data) }
+
+ it 'correctly migrates up and down' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(alerts_service_data.create!(service_id: 1)).to be_a alerts_service_data
+ }
+
+ migration.after -> {
+ expect { alerts_service_data.create!(service_id: 1) }
+ .to raise_error(ActiveRecord::StatementInvalid, /UndefinedTable/)
+ }
+ end
+ end
+end
diff --git a/spec/migrations/encrypt_feature_flags_clients_tokens_spec.rb b/spec/migrations/encrypt_feature_flags_clients_tokens_spec.rb
index ad83119f324..c705515ce98 100644
--- a/spec/migrations/encrypt_feature_flags_clients_tokens_spec.rb
+++ b/spec/migrations/encrypt_feature_flags_clients_tokens_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe EncryptFeatureFlagsClientsTokens do
let(:feature_flags_clients) { table(:operations_feature_flags_clients) }
let(:projects) { table(:projects) }
let(:plaintext) { "secret-token" }
- let(:ciphertext) { Gitlab::CryptoHelper.aes256_gcm_encrypt(plaintext) }
+ let(:ciphertext) { Gitlab::CryptoHelper.aes256_gcm_encrypt(plaintext, nonce: Gitlab::CryptoHelper::AES256_GCM_IV_STATIC) }
describe '#up' do
it 'keeps plaintext token the same and populates token_encrypted if not present' do
diff --git a/spec/migrations/remove_alerts_service_records_again_spec.rb b/spec/migrations/remove_alerts_service_records_again_spec.rb
new file mode 100644
index 00000000000..963b54848f9
--- /dev/null
+++ b/spec/migrations/remove_alerts_service_records_again_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20210205214003_remove_alerts_service_records_again.rb')
+
+RSpec.describe RemoveAlertsServiceRecordsAgain do
+ let(:services) { table(:services) }
+
+ before do
+ 5.times { services.create!(type: 'AlertsService') }
+ services.create!(type: 'SomeOtherType')
+ end
+
+ it 'removes services records of type AlertsService and corresponding data', :aggregate_failures do
+ expect(services.count).to eq(6)
+
+ migrate!
+
+ expect(services.count).to eq(1)
+ expect(services.first.type).to eq('SomeOtherType')
+ expect(services.where(type: 'AlertsService')).to be_empty
+ end
+end
diff --git a/spec/migrations/schedule_migrate_security_scans_spec.rb b/spec/migrations/schedule_migrate_security_scans_spec.rb
index 61b14f239ae..eb86a910611 100644
--- a/spec/migrations/schedule_migrate_security_scans_spec.rb
+++ b/spec/migrations/schedule_migrate_security_scans_spec.rb
@@ -3,7 +3,6 @@
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20200217225719_schedule_migrate_security_scans.rb')
-# rubocop: disable RSpec/FactoriesInMigrationSpecs
RSpec.describe ScheduleMigrateSecurityScans, :sidekiq do
let(:migration) { described_class.new }
let(:namespaces) { table(:namespaces) }
@@ -13,7 +12,7 @@ RSpec.describe ScheduleMigrateSecurityScans, :sidekiq do
let(:namespace) { namespaces.create!(name: "foo", path: "bar") }
let(:project) { projects.create!(namespace_id: namespace.id) }
- let(:build) { builds.create! }
+ let(:job) { builds.create! }
before do
stub_const("#{described_class.name}::BATCH_SIZE", 1)
@@ -35,8 +34,8 @@ RSpec.describe ScheduleMigrateSecurityScans, :sidekiq do
end
context 'has security job artifacts' do
- let!(:job_artifact_1) { job_artifacts.create!(project_id: project.id, job_id: build.id, file_type: 5) }
- let!(:job_artifact_2) { job_artifacts.create!(project_id: project.id, job_id: build.id, file_type: 8) }
+ let!(:job_artifact_1) { job_artifacts.create!(project_id: project.id, job_id: job.id, file_type: 5) }
+ let!(:job_artifact_2) { job_artifacts.create!(project_id: project.id, job_id: job.id, file_type: 8) }
it 'schedules migration of security scans' do
Sidekiq::Testing.fake! do
@@ -52,8 +51,8 @@ RSpec.describe ScheduleMigrateSecurityScans, :sidekiq do
end
context 'has non-security job artifacts' do
- let!(:job_artifact_1) { job_artifacts.create!(project_id: project.id, job_id: build.id, file_type: 4) }
- let!(:job_artifact_2) { job_artifacts.create!(project_id: project.id, job_id: build.id, file_type: 9) }
+ let!(:job_artifact_1) { job_artifacts.create!(project_id: project.id, job_id: job.id, file_type: 4) }
+ let!(:job_artifact_2) { job_artifacts.create!(project_id: project.id, job_id: job.id, file_type: 9) }
it 'schedules migration of security scans' do
Sidekiq::Testing.fake! do
diff --git a/spec/migrations/schedule_populate_issue_email_participants_spec.rb b/spec/migrations/schedule_populate_issue_email_participants_spec.rb
new file mode 100644
index 00000000000..a3f18617b70
--- /dev/null
+++ b/spec/migrations/schedule_populate_issue_email_participants_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20201128210234_schedule_populate_issue_email_participants.rb')
+
+RSpec.describe SchedulePopulateIssueEmailParticipants do
+ let!(:namespace) { table(:namespaces).create!(name: 'namespace', path: 'namespace') }
+ let!(:project) { table(:projects).create!(id: 1, namespace_id: namespace.id) }
+ let!(:issue1) { table(:issues).create!(id: 1, project_id: project.id, service_desk_reply_to: "a@gitlab.com") }
+ let!(:issue2) { table(:issues).create!(id: 2, project_id: project.id) }
+ let!(:issue3) { table(:issues).create!(id: 3, project_id: project.id, service_desk_reply_to: "b@gitlab.com") }
+ let!(:issue4) { table(:issues).create!(id: 4, project_id: project.id, service_desk_reply_to: "c@gitlab.com") }
+ let!(:issue5) { table(:issues).create!(id: 5, project_id: project.id, service_desk_reply_to: "d@gitlab.com") }
+ let(:issue_email_participants) { table(:issue_email_participants) }
+
+ it 'correctly schedules background migrations' do
+ stub_const("#{described_class.name}::BATCH_SIZE", 2)
+
+ Sidekiq::Testing.fake! do
+ freeze_time do
+ migrate!
+
+ expect(described_class::MIGRATION)
+ .to be_scheduled_delayed_migration(2.minutes, 1, 3)
+
+ expect(described_class::MIGRATION)
+ .to be_scheduled_delayed_migration(4.minutes, 4, 5)
+
+ expect(BackgroundMigrationWorker.jobs.size).to eq(2)
+ end
+ end
+ end
+end
diff --git a/spec/models/active_session_spec.rb b/spec/models/active_session_spec.rb
index f0bae3f29c0..51435cc4342 100644
--- a/spec/models/active_session_spec.rb
+++ b/spec/models/active_session_spec.rb
@@ -358,7 +358,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
it 'calls .destroy_sessions' do
expect(ActiveSession).to(
receive(:destroy_sessions)
- .with(anything, user, [active_session.public_id, rack_session.public_id, rack_session.private_id]))
+ .with(anything, user, [encrypted_active_session_id, rack_session.public_id, rack_session.private_id]))
subject
end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 4755d700d72..9a4dd2c799b 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -114,6 +114,21 @@ RSpec.describe ApplicationSetting do
it { is_expected.to allow_value(nil).for(:repository_storages_weighted_default) }
it { is_expected.not_to allow_value({ default: 100, shouldntexist: 50 }).for(:repository_storages_weighted) }
+ it { is_expected.to allow_value(400).for(:notes_create_limit) }
+ it { is_expected.not_to allow_value('two').for(:notes_create_limit) }
+ it { is_expected.not_to allow_value(nil).for(:notes_create_limit) }
+ it { is_expected.not_to allow_value(5.5).for(:notes_create_limit) }
+ it { is_expected.not_to allow_value(-2).for(:notes_create_limit) }
+
+ def many_usernames(num = 100)
+ Array.new(num) { |i| "username#{i}" }
+ end
+
+ it { is_expected.to allow_value(many_usernames(100)).for(:notes_create_limit_allowlist) }
+ it { is_expected.not_to allow_value(many_usernames(101)).for(:notes_create_limit_allowlist) }
+ it { is_expected.not_to allow_value(nil).for(:notes_create_limit_allowlist) }
+ it { is_expected.to allow_value([]).for(:notes_create_limit_allowlist) }
+
context 'help_page_documentation_base_url validations' do
it { is_expected.to allow_value(nil).for(:help_page_documentation_base_url) }
it { is_expected.to allow_value('https://docs.gitlab.com').for(:help_page_documentation_base_url) }
@@ -635,28 +650,28 @@ RSpec.describe ApplicationSetting do
end
end
- describe '#asset_proxy_whitelist' do
+ describe '#asset_proxy_allowlist' do
context 'when given an Array' do
it 'sets the domains and adds current running host' do
- setting.asset_proxy_whitelist = ['example.com', 'assets.example.com']
- expect(setting.asset_proxy_whitelist).to eq(['example.com', 'assets.example.com', 'localhost'])
+ setting.asset_proxy_allowlist = ['example.com', 'assets.example.com']
+ expect(setting.asset_proxy_allowlist).to eq(['example.com', 'assets.example.com', 'localhost'])
end
end
context 'when given a String' do
it 'sets multiple domains with spaces' do
- setting.asset_proxy_whitelist = 'example.com *.example.com'
- expect(setting.asset_proxy_whitelist).to eq(['example.com', '*.example.com', 'localhost'])
+ setting.asset_proxy_allowlist = 'example.com *.example.com'
+ expect(setting.asset_proxy_allowlist).to eq(['example.com', '*.example.com', 'localhost'])
end
it 'sets multiple domains with newlines and a space' do
- setting.asset_proxy_whitelist = "example.com\n *.example.com"
- expect(setting.asset_proxy_whitelist).to eq(['example.com', '*.example.com', 'localhost'])
+ setting.asset_proxy_allowlist = "example.com\n *.example.com"
+ expect(setting.asset_proxy_allowlist).to eq(['example.com', '*.example.com', 'localhost'])
end
it 'sets multiple domains with commas' do
- setting.asset_proxy_whitelist = "example.com, *.example.com"
- expect(setting.asset_proxy_whitelist).to eq(['example.com', '*.example.com', 'localhost'])
+ setting.asset_proxy_allowlist = "example.com, *.example.com"
+ expect(setting.asset_proxy_allowlist).to eq(['example.com', '*.example.com', 'localhost'])
end
end
end
@@ -949,6 +964,50 @@ RSpec.describe ApplicationSetting do
end
end
+ describe 'kroki_format_supported?' do
+ it 'returns true when Excalidraw is enabled' do
+ subject.kroki_formats_excalidraw = true
+ expect(subject.kroki_format_supported?('excalidraw')).to eq(true)
+ end
+
+ it 'returns true when BlockDiag is enabled' do
+ subject.kroki_formats_blockdiag = true
+ # format "blockdiag" aggregates multiple diagram types: actdiag, blockdiag, nwdiag...
+ expect(subject.kroki_format_supported?('actdiag')).to eq(true)
+ expect(subject.kroki_format_supported?('blockdiag')).to eq(true)
+ end
+
+ it 'returns false when BlockDiag is disabled' do
+ subject.kroki_formats_blockdiag = false
+ # format "blockdiag" aggregates multiple diagram types: actdiag, blockdiag, nwdiag...
+ expect(subject.kroki_format_supported?('actdiag')).to eq(false)
+ expect(subject.kroki_format_supported?('blockdiag')).to eq(false)
+ end
+
+ it 'returns false when the diagram type is optional and not enabled' do
+ expect(subject.kroki_format_supported?('bpmn')).to eq(false)
+ end
+
+ it 'returns true when the diagram type is enabled by default' do
+ expect(subject.kroki_format_supported?('vegalite')).to eq(true)
+ expect(subject.kroki_format_supported?('nomnoml')).to eq(true)
+ expect(subject.kroki_format_supported?('unknown-diagram-type')).to eq(false)
+ end
+
+ it 'returns false when the diagram type is unknown' do
+ expect(subject.kroki_format_supported?('unknown-diagram-type')).to eq(false)
+ end
+ end
+
+ describe 'kroki_formats' do
+ it 'returns the value for kroki_formats' do
+ subject.kroki_formats = { blockdiag: true, bpmn: false, excalidraw: true }
+ expect(subject.kroki_formats_blockdiag).to eq(true)
+ expect(subject.kroki_formats_bpmn).to eq(false)
+ expect(subject.kroki_formats_excalidraw).to eq(true)
+ end
+ end
+
it 'does not allow to set weight for non existing storage' do
setting.repository_storages_weighted = { invalid_storage: 100 }
diff --git a/spec/models/bulk_imports/entity_spec.rb b/spec/models/bulk_imports/entity_spec.rb
index 0732b671729..e5ab96ca514 100644
--- a/spec/models/bulk_imports/entity_spec.rb
+++ b/spec/models/bulk_imports/entity_spec.rb
@@ -81,6 +81,37 @@ RSpec.describe BulkImports::Entity, type: :model do
expect(entity.errors).to include(:parent)
end
end
+
+ context 'validate destination namespace of a group_entity' do
+ it 'is invalid if destination namespace is the source namespace' do
+ group_a = create(:group, path: 'group_a')
+
+ entity = build(
+ :bulk_import_entity,
+ :group_entity,
+ source_full_path: group_a.full_path,
+ destination_namespace: group_a.full_path
+ )
+
+ expect(entity).not_to be_valid
+ expect(entity.errors).to include(:destination_namespace)
+ end
+
+ it 'is invalid if destination namespace is a descendant of the source' do
+ group_a = create(:group, path: 'group_a')
+ group_b = create(:group, parent: group_a, path: 'group_b')
+
+ entity = build(
+ :bulk_import_entity,
+ :group_entity,
+ source_full_path: group_a.full_path,
+ destination_namespace: group_b.full_path
+ )
+
+ expect(entity).not_to be_valid
+ expect(entity.errors).to include(:destination_namespace)
+ end
+ end
end
describe "#update_tracker_for" do
diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb
index 4f09f6f1da4..b50e4204e0a 100644
--- a/spec/models/ci/bridge_spec.rb
+++ b/spec/models/ci/bridge_spec.rb
@@ -80,6 +80,14 @@ RSpec.describe Ci::Bridge do
end
end
+ it "schedules downstream pipeline creation when the status is waiting for resource" do
+ bridge.status = :waiting_for_resource
+
+ expect(bridge).to receive(:schedule_downstream_pipeline!)
+
+ bridge.enqueue_waiting_for_resource!
+ end
+
it 'raises error when the status is failed' do
bridge.status = :failed
diff --git a/spec/models/ci/build_dependencies_spec.rb b/spec/models/ci/build_dependencies_spec.rb
index c5f56dbe5bc..e343ec0e698 100644
--- a/spec/models/ci/build_dependencies_spec.rb
+++ b/spec/models/ci/build_dependencies_spec.rb
@@ -18,6 +18,10 @@ RSpec.describe Ci::BuildDependencies do
let!(:rubocop_test) { create(:ci_build, pipeline: pipeline, name: 'rubocop', stage_idx: 1, stage: 'test') }
let!(:staging) { create(:ci_build, pipeline: pipeline, name: 'staging', stage_idx: 2, stage: 'deploy') }
+ before do
+ stub_feature_flags(ci_validate_build_dependencies_override: false)
+ end
+
describe '#local' do
subject { described_class.new(job).local }
@@ -360,4 +364,28 @@ RSpec.describe Ci::BuildDependencies do
expect(subject).to contain_exactly(1, 2, 3, 4)
end
end
+
+ describe '#valid?' do
+ subject { described_class.new(job).valid? }
+
+ let(:job) { rspec_test }
+
+ it { is_expected.to eq(true) }
+
+ context 'when a local dependency is invalid' do
+ before do
+ build.update_column(:erased_at, Time.current)
+ end
+
+ it { is_expected.to eq(false) }
+
+ context 'when ci_validate_build_dependencies_override feature flag is enabled' do
+ before do
+ stub_feature_flags(ci_validate_build_dependencies_override: job.project)
+ end
+
+ it { is_expected.to eq(true) }
+ end
+ end
+ end
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index c2029b9240b..4ad7ce70a44 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -1185,60 +1185,6 @@ RSpec.describe Ci::Build do
end
end
- describe 'state transition with resource group' do
- let(:resource_group) { create(:ci_resource_group, project: project) }
-
- context 'when build status is created' do
- let(:build) { create(:ci_build, :created, project: project, resource_group: resource_group) }
-
- it 'is waiting for resource when build is enqueued' do
- expect(Ci::ResourceGroups::AssignResourceFromResourceGroupWorker).to receive(:perform_async).with(resource_group.id)
-
- expect { build.enqueue! }.to change { build.status }.from('created').to('waiting_for_resource')
-
- expect(build.waiting_for_resource_at).not_to be_nil
- end
-
- context 'when build is waiting for resource' do
- before do
- build.update_column(:status, 'waiting_for_resource')
- end
-
- it 'is enqueued when build requests resource' do
- expect { build.enqueue_waiting_for_resource! }.to change { build.status }.from('waiting_for_resource').to('pending')
- end
-
- it 'releases a resource when build finished' do
- expect(build.resource_group).to receive(:release_resource_from).with(build).and_call_original
- expect(Ci::ResourceGroups::AssignResourceFromResourceGroupWorker).to receive(:perform_async).with(build.resource_group_id)
-
- build.enqueue_waiting_for_resource!
- build.success!
- end
-
- context 'when build has prerequisites' do
- before do
- allow(build).to receive(:any_unmet_prerequisites?) { true }
- end
-
- it 'is preparing when build is enqueued' do
- expect { build.enqueue_waiting_for_resource! }.to change { build.status }.from('waiting_for_resource').to('preparing')
- end
- end
-
- context 'when there are no available resources' do
- before do
- resource_group.assign_resource_to(create(:ci_build))
- end
-
- it 'stays as waiting for resource when build requests resource' do
- expect { build.enqueue_waiting_for_resource }.not_to change { build.status }
- end
- end
- end
- end
- end
-
describe '#on_stop' do
subject { build.on_stop }
@@ -1914,7 +1860,7 @@ RSpec.describe Ci::Build do
subject { build.artifacts_file_for_type(file_type) }
it 'queries artifacts for type' do
- expect(build).to receive_message_chain(:job_artifacts, :find_by).with(file_type: Ci::JobArtifact.file_types[file_type])
+ expect(build).to receive_message_chain(:job_artifacts, :find_by).with(file_type: [Ci::JobArtifact.file_types[file_type]])
subject
end
@@ -3605,7 +3551,7 @@ RSpec.describe Ci::Build do
context 'when validates for dependencies is enabled' do
before do
- stub_feature_flags(ci_disable_validates_dependencies: false)
+ stub_feature_flags(ci_validate_build_dependencies_override: false)
end
let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0) }
@@ -3633,7 +3579,7 @@ RSpec.describe Ci::Build do
let(:options) { { dependencies: ['test'] } }
before do
- stub_feature_flags(ci_disable_validates_dependencies: true)
+ stub_feature_flags(ci_validate_build_dependencies_override: true)
end
it_behaves_like 'validation is not active'
@@ -4096,18 +4042,6 @@ RSpec.describe Ci::Build do
expect(coverage_report.files.keys).to match_array(['src/main/java/com/example/javademo/User.java'])
end
-
- context 'and smart_cobertura_parser feature flag is disabled' do
- before do
- stub_feature_flags(smart_cobertura_parser: false)
- end
-
- it 'parses blobs and add the results to the coverage report with unmodified paths' do
- expect { subject }.not_to raise_error
-
- expect(coverage_report.files.keys).to match_array(['com/example/javademo/User.java'])
- end
- end
end
context 'when there is a corrupted Cobertura coverage report' do
@@ -4922,14 +4856,6 @@ RSpec.describe Ci::Build do
it_behaves_like 'drops the build without changing allow_failure'
end
-
- context 'when ci_allow_failure_with_exit_codes is disabled' do
- before do
- stub_feature_flags(ci_allow_failure_with_exit_codes: false)
- end
-
- it_behaves_like 'drops the build without changing allow_failure'
- end
end
end
diff --git a/spec/models/ci/build_trace_chunk_spec.rb b/spec/models/ci/build_trace_chunk_spec.rb
index 75ed5939724..3d728b9335e 100644
--- a/spec/models/ci/build_trace_chunk_spec.rb
+++ b/spec/models/ci/build_trace_chunk_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
it_behaves_like 'having unique enum values'
before do
- stub_feature_flags(ci_enable_live_trace: true)
+ stub_feature_flags(ci_enable_live_trace: true, gitlab_ci_trace_read_consistency: true)
stub_artifacts_object_storage
end
diff --git a/spec/models/ci/build_trace_chunks/fog_spec.rb b/spec/models/ci/build_trace_chunks/fog_spec.rb
index bc96e2584cf..d9e9533fb26 100644
--- a/spec/models/ci/build_trace_chunks/fog_spec.rb
+++ b/spec/models/ci/build_trace_chunks/fog_spec.rb
@@ -98,27 +98,6 @@ RSpec.describe Ci::BuildTraceChunks::Fog do
expect(data_store.data(model)).to eq new_data
end
-
- context 'when ci_live_trace_use_fog_attributes flag is disabled' do
- before do
- stub_feature_flags(ci_live_trace_use_fog_attributes: false)
- end
-
- it 'does not pass along Fog attributes' do
- expect_next_instance_of(Fog::AWS::Storage::Files) do |files|
- expect(files).to receive(:create).with(
- key: anything,
- body: new_data
- ).and_call_original
- end
-
- expect(data_store.data(model)).to be_nil
-
- data_store.set_data(model, new_data)
-
- expect(data_store.data(model)).to eq new_data
- end
- end
end
end
end
diff --git a/spec/models/ci/daily_build_group_report_result_spec.rb b/spec/models/ci/daily_build_group_report_result_spec.rb
index f16396d62c9..f6e6a6a5e02 100644
--- a/spec/models/ci/daily_build_group_report_result_spec.rb
+++ b/spec/models/ci/daily_build_group_report_result_spec.rb
@@ -8,6 +8,7 @@ RSpec.describe Ci::DailyBuildGroupReportResult do
describe 'associations' do
it { is_expected.to belong_to(:last_pipeline) }
it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:group) }
end
describe 'validations' do
@@ -83,8 +84,9 @@ RSpec.describe Ci::DailyBuildGroupReportResult do
end
describe 'scopes' do
- let_it_be(:project) { create(:project) }
- let(:recent_build_group_report_result) { create(:ci_daily_build_group_report_result, project: project) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+ let(:recent_build_group_report_result) { create(:ci_daily_build_group_report_result, project: project, group: group) }
let(:old_build_group_report_result) do
create(:ci_daily_build_group_report_result, date: 1.week.ago, project: project)
end
@@ -97,6 +99,43 @@ RSpec.describe Ci::DailyBuildGroupReportResult do
end
end
+ describe '.by_group' do
+ subject { described_class.by_group(group) }
+
+ it 'returns records by group' do
+ expect(subject).to contain_exactly(recent_build_group_report_result)
+ end
+ end
+
+ describe '.by_ref_path' do
+ subject(:coverages) { described_class.by_ref_path(recent_build_group_report_result.ref_path) }
+
+ it 'returns coverages by ref_path' do
+ expect(coverages).to contain_exactly(recent_build_group_report_result, old_build_group_report_result)
+ end
+ end
+
+ describe '.ordered_by_date_and_group_name' do
+ subject(:coverages) { described_class.ordered_by_date_and_group_name }
+
+ it 'returns coverages ordered by data and group name' do
+ expect(subject).to contain_exactly(recent_build_group_report_result, old_build_group_report_result)
+ end
+ end
+
+ describe '.by_dates' do
+ subject(:coverages) { described_class.by_dates(start_date, end_date) }
+
+ context 'when daily coverages exist during those dates' do
+ let(:start_date) { 1.day.ago.to_date.to_s }
+ let(:end_date) { Date.current.to_s }
+
+ it 'returns coverages' do
+ expect(coverages).to contain_exactly(recent_build_group_report_result)
+ end
+ end
+ end
+
describe '.with_coverage' do
subject { described_class.with_coverage }
diff --git a/spec/models/ci/pipeline_artifact_spec.rb b/spec/models/ci/pipeline_artifact_spec.rb
index 8cbace845a9..3fe09f05cab 100644
--- a/spec/models/ci/pipeline_artifact_spec.rb
+++ b/spec/models/ci/pipeline_artifact_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Ci::PipelineArtifact, type: :model do
- let(:coverage_report) { create(:ci_pipeline_artifact) }
+ let(:coverage_report) { create(:ci_pipeline_artifact, :with_coverage_report) }
describe 'associations' do
it { is_expected.to belong_to(:pipeline) }
@@ -15,7 +15,7 @@ RSpec.describe Ci::PipelineArtifact, type: :model do
it_behaves_like 'UpdateProjectStatistics' do
let_it_be(:pipeline, reload: true) { create(:ci_pipeline) }
- subject { build(:ci_pipeline_artifact, pipeline: pipeline) }
+ subject { build(:ci_pipeline_artifact, :with_code_coverage_with_multiple_files, pipeline: pipeline) }
end
describe 'validations' do
@@ -51,7 +51,7 @@ RSpec.describe Ci::PipelineArtifact, type: :model do
end
describe 'file is being stored' do
- subject { create(:ci_pipeline_artifact) }
+ subject { create(:ci_pipeline_artifact, :with_coverage_report) }
context 'when existing object has local store' do
it_behaves_like 'mounted file in local store'
@@ -68,7 +68,7 @@ RSpec.describe Ci::PipelineArtifact, type: :model do
end
context 'when file contains multi-byte characters' do
- let(:coverage_report_multibyte) { create(:ci_pipeline_artifact, :with_multibyte_characters) }
+ let(:coverage_report_multibyte) { create(:ci_pipeline_artifact, :with_coverage_multibyte_characters) }
it 'sets the size in bytesize' do
expect(coverage_report_multibyte.size).to eq(14)
@@ -76,48 +76,118 @@ RSpec.describe Ci::PipelineArtifact, type: :model do
end
end
- describe '.has_code_coverage?' do
- subject { Ci::PipelineArtifact.has_code_coverage? }
+ describe '.report_exists?' do
+ subject(:pipeline_artifact) { Ci::PipelineArtifact.report_exists?(file_type) }
- context 'when pipeline artifact has a code coverage' do
- let!(:pipeline_artifact) { create(:ci_pipeline_artifact) }
+ context 'when file_type is code_coverage' do
+ let(:file_type) { :code_coverage }
+
+ context 'when pipeline artifact has a coverage report' do
+ let!(:pipeline_artifact) { create(:ci_pipeline_artifact, :with_coverage_report) }
+
+ it 'returns true' do
+ expect(pipeline_artifact).to be_truthy
+ end
+ end
- it 'returns true' do
- expect(subject).to be_truthy
+ context 'when pipeline artifact does not have a coverage report' do
+ it 'returns false' do
+ expect(pipeline_artifact).to be_falsey
+ end
end
end
- context 'when pipeline artifact does not have a code coverage' do
+ context 'when file_type is code_quality_mr_diff' do
+ let(:file_type) { :code_quality_mr_diff }
+
+ context 'when pipeline artifact has a codequality mr diff report' do
+ let!(:pipeline_artifact) { create(:ci_pipeline_artifact, :with_codequality_mr_diff_report) }
+
+ it 'returns true' do
+ expect(pipeline_artifact).to be_truthy
+ end
+ end
+
+ context 'when pipeline artifact does not have a codequality mr diff report' do
+ it 'returns false' do
+ expect(pipeline_artifact).to be_falsey
+ end
+ end
+ end
+
+ context 'when file_type is nil' do
+ let(:file_type) { nil }
+
it 'returns false' do
- expect(subject).to be_falsey
+ expect(pipeline_artifact).to be_falsey
end
end
end
- describe '.find_with_code_coverage' do
- subject { Ci::PipelineArtifact.find_with_code_coverage }
+ describe '.find_by_file_type' do
+ subject(:pipeline_artifact) { Ci::PipelineArtifact.find_by_file_type(file_type) }
- context 'when pipeline artifact has a coverage report' do
- let!(:coverage_report) { create(:ci_pipeline_artifact) }
+ context 'when file_type is code_coverage' do
+ let(:file_type) { :code_coverage }
+
+ context 'when pipeline artifact has a coverage report' do
+ let!(:coverage_report) { create(:ci_pipeline_artifact, :with_coverage_report) }
- it 'returns a pipeline artifact with a code coverage' do
- expect(subject.file_type).to eq('code_coverage')
+ it 'returns a pipeline artifact with a coverage report' do
+ expect(pipeline_artifact.file_type).to eq('code_coverage')
+ end
+ end
+
+ context 'when pipeline artifact does not have a coverage report' do
+ it 'returns nil' do
+ expect(pipeline_artifact).to be_nil
+ end
+ end
+ end
+
+ context 'when file_type is code_quality_mr_diff' do
+ let(:file_type) { :code_quality_mr_diff }
+
+ context 'when pipeline artifact has a quality report' do
+ let!(:coverage_report) { create(:ci_pipeline_artifact, :with_codequality_mr_diff_report) }
+
+ it 'returns a pipeline artifact with a quality report' do
+ expect(pipeline_artifact.file_type).to eq('code_quality_mr_diff')
+ end
+ end
+
+ context 'when pipeline artifact does not have a quality report' do
+ it 'returns nil' do
+ expect(pipeline_artifact).to be_nil
+ end
end
end
- context 'when pipeline artifact does not have a coverage report' do
+ context 'when file_type is nil' do
+ let(:file_type) { nil }
+
it 'returns nil' do
- expect(subject).to be_nil
+ expect(pipeline_artifact).to be_nil
end
end
end
describe '#present' do
- subject { coverage_report.present }
+ subject(:presenter) { report.present }
context 'when file_type is code_coverage' do
+ let(:report) { coverage_report }
+
it 'uses code coverage presenter' do
- expect(subject.present).to be_kind_of(Ci::PipelineArtifacts::CodeCoveragePresenter)
+ expect(presenter).to be_kind_of(Ci::PipelineArtifacts::CodeCoveragePresenter)
+ end
+ end
+
+ context 'when file_type is code_quality_mr_diff' do
+ let(:report) { create(:ci_pipeline_artifact, :with_codequality_mr_diff_report) }
+
+ it 'uses code codequality mr diff presenter' do
+ expect(presenter).to be_kind_of(Ci::PipelineArtifacts::CodeQualityMrDiffPresenter)
end
end
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 140527e4414..94943fb3644 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -1263,26 +1263,6 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
pipeline.send(event)
end
-
- context 'the feature is disabled' do
- it 'does not trigger a worker' do
- stub_feature_flags(jira_sync_builds: false)
-
- expect(worker).not_to receive(:perform_async)
-
- pipeline.send(event)
- end
- end
-
- context 'the feature is enabled for this project' do
- it 'does trigger a worker' do
- stub_feature_flags(jira_sync_builds: pipeline.project)
-
- expect(worker).to receive(:perform_async)
-
- pipeline.send(event)
- end
- end
end
end
end
@@ -2018,13 +1998,34 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
is_expected.to be_falsey
end
end
+
+ context 'bridge which is allowed to fail fails' do
+ before do
+ create :ci_bridge, :allowed_to_fail, :failed, pipeline: pipeline, name: 'rubocop'
+ end
+
+ it 'returns true' do
+ is_expected.to be_truthy
+ end
+ end
+
+ context 'bridge which is allowed to fail is successful' do
+ before do
+ create :ci_bridge, :allowed_to_fail, :success, pipeline: pipeline, name: 'rubocop'
+ end
+
+ it 'returns false' do
+ is_expected.to be_falsey
+ end
+ end
end
describe '#number_of_warnings' do
it 'returns the number of warnings' do
create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline, name: 'rubocop')
+ create(:ci_bridge, :allowed_to_fail, :failed, pipeline: pipeline, name: 'rubocop')
- expect(pipeline.number_of_warnings).to eq(1)
+ expect(pipeline.number_of_warnings).to eq(2)
end
it 'supports eager loading of the number of warnings' do
@@ -2322,7 +2323,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
context 'on waiting for resource' do
before do
- allow(build).to receive(:requires_resource?) { true }
+ allow(build).to receive(:with_resource_group?) { true }
allow(Ci::ResourceGroups::AssignResourceFromResourceGroupWorker).to receive(:perform_async)
build.enqueue
@@ -3389,7 +3390,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
describe '#batch_lookup_report_artifact_for_file_type' do
context 'with code quality report artifact' do
- let(:pipeline) { create(:ci_pipeline, :with_codequality_report, project: project) }
+ let(:pipeline) { create(:ci_pipeline, :with_codequality_reports, project: project) }
it "returns the code quality artifact" do
expect(pipeline.batch_lookup_report_artifact_for_file_type(:codequality)).to eq(pipeline.job_artifacts.sample)
@@ -3511,6 +3512,66 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
end
+ describe '#has_codequality_mr_diff_report?' do
+ subject { pipeline.has_codequality_mr_diff_report? }
+
+ context 'when pipeline has a codequality mr diff report' do
+ let(:pipeline) { create(:ci_pipeline, :with_codequality_mr_diff_report, :running, project: project) }
+
+ it { expect(subject).to be_truthy }
+ end
+
+ context 'when pipeline does not have a codequality mr diff report' do
+ let(:pipeline) { create(:ci_pipeline, :success, project: project) }
+
+ it { expect(subject).to be_falsey }
+ end
+ end
+
+ describe '#can_generate_codequality_reports?' do
+ subject { pipeline.can_generate_codequality_reports? }
+
+ context 'when pipeline has builds with codequality reports' do
+ before do
+ create(:ci_build, :codequality_reports, pipeline: pipeline, project: project)
+ end
+
+ context 'when pipeline status is running' do
+ let(:pipeline) { create(:ci_pipeline, :running, project: project) }
+
+ it { expect(subject).to be_falsey }
+ end
+
+ context 'when pipeline status is success' do
+ let(:pipeline) { create(:ci_pipeline, :success, project: project) }
+
+ it 'can generate a codequality report' do
+ expect(subject).to be_truthy
+ end
+
+ context 'when feature is disabled' do
+ before do
+ stub_feature_flags(codequality_mr_diff: false)
+ end
+
+ it 'can not generate a codequality report' do
+ expect(subject).to be_falsey
+ end
+ end
+ end
+ end
+
+ context 'when pipeline does not have builds with codequality reports' do
+ before do
+ create(:ci_build, :artifacts, pipeline: pipeline, project: project)
+ end
+
+ let(:pipeline) { create(:ci_pipeline, :success, project: project) }
+
+ it { expect(subject).to be_falsey }
+ end
+ end
+
describe '#test_report_summary' do
subject { pipeline.test_report_summary }
diff --git a/spec/models/ci/processable_spec.rb b/spec/models/ci/processable_spec.rb
index 35764e2bbbe..6290f4aef16 100644
--- a/spec/models/ci/processable_spec.rb
+++ b/spec/models/ci/processable_spec.rb
@@ -122,4 +122,58 @@ RSpec.describe Ci::Processable do
it { is_expected.to be_empty }
end
end
+
+ describe 'state transition with resource group' do
+ let(:resource_group) { create(:ci_resource_group, project: project) }
+
+ context 'when build status is created' do
+ let(:build) { create(:ci_build, :created, project: project, resource_group: resource_group) }
+
+ it 'is waiting for resource when build is enqueued' do
+ expect(Ci::ResourceGroups::AssignResourceFromResourceGroupWorker).to receive(:perform_async).with(resource_group.id)
+
+ expect { build.enqueue! }.to change { build.status }.from('created').to('waiting_for_resource')
+
+ expect(build.waiting_for_resource_at).not_to be_nil
+ end
+
+ context 'when build is waiting for resource' do
+ before do
+ build.update_column(:status, 'waiting_for_resource')
+ end
+
+ it 'is enqueued when build requests resource' do
+ expect { build.enqueue_waiting_for_resource! }.to change { build.status }.from('waiting_for_resource').to('pending')
+ end
+
+ it 'releases a resource when build finished' do
+ expect(build.resource_group).to receive(:release_resource_from).with(build).and_call_original
+ expect(Ci::ResourceGroups::AssignResourceFromResourceGroupWorker).to receive(:perform_async).with(build.resource_group_id)
+
+ build.enqueue_waiting_for_resource!
+ build.success!
+ end
+
+ context 'when build has prerequisites' do
+ before do
+ allow(build).to receive(:any_unmet_prerequisites?) { true }
+ end
+
+ it 'is preparing when build is enqueued' do
+ expect { build.enqueue_waiting_for_resource! }.to change { build.status }.from('waiting_for_resource').to('preparing')
+ end
+ end
+
+ context 'when there are no available resources' do
+ before do
+ resource_group.assign_resource_to(create(:ci_build))
+ end
+
+ it 'stays as waiting for resource when build requests resource' do
+ expect { build.enqueue_waiting_for_resource }.not_to change { build.status }
+ end
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/ci/resource_group_spec.rb b/spec/models/ci/resource_group_spec.rb
index 9f72d1a82e5..50a786419f2 100644
--- a/spec/models/ci/resource_group_spec.rb
+++ b/spec/models/ci/resource_group_spec.rb
@@ -32,12 +32,12 @@ RSpec.describe Ci::ResourceGroup do
let(:build) { create(:ci_build) }
let(:resource_group) { create(:ci_resource_group) }
- it 'retains resource for the build' do
- expect(resource_group.resources.first.build).to be_nil
+ it 'retains resource for the processable' do
+ expect(resource_group.resources.first.processable).to be_nil
is_expected.to eq(true)
- expect(resource_group.resources.first.build).to eq(build)
+ expect(resource_group.resources.first.processable).to eq(build)
end
context 'when there are no free resources' do
@@ -51,7 +51,7 @@ RSpec.describe Ci::ResourceGroup do
end
context 'when the build has already retained a resource' do
- let!(:another_resource) { create(:ci_resource, resource_group: resource_group, build: build) }
+ let!(:another_resource) { create(:ci_resource, resource_group: resource_group, processable: build) }
it 'fails to retain resource' do
expect { subject }.to raise_error(ActiveRecord::RecordNotUnique)
@@ -71,11 +71,11 @@ RSpec.describe Ci::ResourceGroup do
end
it 'releases resource from the build' do
- expect(resource_group.resources.first.build).to eq(build)
+ expect(resource_group.resources.first.processable).to eq(build)
is_expected.to eq(true)
- expect(resource_group.resources.first.build).to be_nil
+ expect(resource_group.resources.first.processable).to be_nil
end
end
diff --git a/spec/models/ci/resource_spec.rb b/spec/models/ci/resource_spec.rb
index 90f26ef2b31..5574f6f82b2 100644
--- a/spec/models/ci/resource_spec.rb
+++ b/spec/models/ci/resource_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe Ci::Resource do
subject { described_class.retained_by(build) }
let(:build) { create(:ci_build) }
- let!(:resource) { create(:ci_resource, build: build) }
+ let!(:resource) { create(:ci_resource, processable: build) }
it 'returns retained resources' do
is_expected.to eq([resource])
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb
index 3d873a1b9c1..0afc491dc73 100644
--- a/spec/models/ci/stage_spec.rb
+++ b/spec/models/ci/stage_spec.rb
@@ -288,6 +288,7 @@ RSpec.describe Ci::Stage, :models do
context 'when stage has warnings' do
before do
create(:ci_build, :failed, :allowed_to_fail, stage_id: stage.id)
+ create(:ci_bridge, :failed, :allowed_to_fail, stage_id: stage.id)
end
describe '#has_warnings?' do
@@ -310,7 +311,7 @@ RSpec.describe Ci::Stage, :models do
expect(synced_queries.count).to eq 1
expect(stage.number_of_warnings.inspect).to include 'BatchLoader'
- expect(stage.number_of_warnings).to eq 1
+ expect(stage.number_of_warnings).to eq 2
end
end
end
diff --git a/spec/models/clusters/agent_spec.rb b/spec/models/clusters/agent_spec.rb
index 49f41570717..a85a72eba0b 100644
--- a/spec/models/clusters/agent_spec.rb
+++ b/spec/models/clusters/agent_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe Clusters::Agent do
subject { create(:cluster_agent) }
+ it { is_expected.to belong_to(:created_by_user).class_name('User').optional }
it { is_expected.to belong_to(:project).class_name('::Project') }
it { is_expected.to have_many(:agent_tokens).class_name('Clusters::AgentToken') }
diff --git a/spec/models/clusters/agent_token_spec.rb b/spec/models/clusters/agent_token_spec.rb
index 9110fdeda52..5cb84ee131a 100644
--- a/spec/models/clusters/agent_token_spec.rb
+++ b/spec/models/clusters/agent_token_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe Clusters::AgentToken do
it { is_expected.to belong_to(:agent).class_name('Clusters::Agent') }
+ it { is_expected.to belong_to(:created_by_user).class_name('User').optional }
describe '#token' do
it 'is generated on save' do
diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb
index fb0613187c5..07e64889b93 100644
--- a/spec/models/clusters/platforms/kubernetes_spec.rb
+++ b/spec/models/clusters/platforms/kubernetes_spec.rb
@@ -17,6 +17,8 @@ RSpec.describe Clusters::Platforms::Kubernetes do
it { is_expected.to delegate_method(:enabled?).to(:cluster) }
it { is_expected.to delegate_method(:provided_by_user?).to(:cluster) }
+ it { is_expected.to nullify_if_blank(:namespace) }
+
it_behaves_like 'having unique enum values'
describe 'before_validation' do
@@ -29,14 +31,6 @@ RSpec.describe Clusters::Platforms::Kubernetes do
expect(kubernetes.namespace).to eq('abc')
end
end
-
- context 'when namespace is blank' do
- let(:namespace) { '' }
-
- it 'nullifies the namespace' do
- expect(kubernetes.namespace).to be_nil
- end
- end
end
describe 'validation' do
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index acbabee9383..a5f02b61132 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -400,6 +400,19 @@ eos
allow(commit).to receive(:safe_message).and_return(message + "\n" + message)
expect(commit.full_title).to eq(message)
end
+
+ it 'truncates html representation if more than 1KiB' do
+ # Commit title is over 2KiB on a single line
+ huge_commit_title = ('panic ' * 350) + 'trailing text'
+
+ allow(commit).to receive(:safe_message).and_return(huge_commit_title)
+
+ commit.refresh_markdown_cache
+ full_title_html = commit.full_title_html
+
+ expect(full_title_html.bytesize).to be < 2.kilobytes
+ expect(full_title_html).not_to include('trailing text')
+ end
end
describe 'description' do
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 532f68c2f18..01da379e001 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -510,6 +510,10 @@ RSpec.describe CommitStatus do
end
describe '#group_name' do
+ before do
+ stub_feature_flags(simplified_commit_status_group_name: false)
+ end
+
using RSpec::Parameterized::TableSyntax
let(:commit_status) do
@@ -557,6 +561,58 @@ RSpec.describe CommitStatus do
is_expected.to eq(group_name)
end
end
+
+ context 'with simplified_commit_status_group_name' do
+ before do
+ stub_feature_flags(simplified_commit_status_group_name: true)
+ end
+
+ where(:name, :group_name) do
+ 'rspec1' | 'rspec1'
+ 'rspec1 0 1' | 'rspec1'
+ 'rspec1 0/2' | 'rspec1'
+ 'rspec:windows' | 'rspec:windows'
+ 'rspec:windows 0' | 'rspec:windows 0'
+ 'rspec:windows 0 2/2' | 'rspec:windows 0'
+ 'rspec:windows 0 test' | 'rspec:windows 0 test'
+ 'rspec:windows 0 test 2/2' | 'rspec:windows 0 test'
+ 'rspec:windows 0 1 2/2' | 'rspec:windows'
+ 'rspec:windows 0 1 [aws] 2/2' | 'rspec:windows'
+ 'rspec:windows 0 1 name [aws] 2/2' | 'rspec:windows 0 1 name'
+ 'rspec:windows 0 1 name' | 'rspec:windows 0 1 name'
+ 'rspec:windows 0 1 name 1/2' | 'rspec:windows 0 1 name'
+ 'rspec:windows 0/1' | 'rspec:windows'
+ 'rspec:windows 0/1 name' | 'rspec:windows 0/1 name'
+ 'rspec:windows 0/1 name 1/2' | 'rspec:windows 0/1 name'
+ 'rspec:windows 0:1' | 'rspec:windows'
+ 'rspec:windows 0:1 name' | 'rspec:windows 0:1 name'
+ 'rspec:windows 10000 20000' | 'rspec:windows'
+ 'rspec:windows 0 : / 1' | 'rspec:windows'
+ 'rspec:windows 0 : / 1 name' | 'rspec:windows 0 : / 1 name'
+ '0 1 name ruby' | '0 1 name ruby'
+ '0 :/ 1 name ruby' | '0 :/ 1 name ruby'
+ 'rspec: [aws]' | 'rspec'
+ 'rspec: [aws] 0/1' | 'rspec'
+ 'rspec: [aws, max memory]' | 'rspec'
+ 'rspec:linux: [aws, max memory, data]' | 'rspec:linux'
+ 'rspec: [inception: [something, other thing], value]' | 'rspec'
+ 'rspec:windows 0/1: [name, other]' | 'rspec:windows'
+ 'rspec:windows: [name, other] 0/1' | 'rspec:windows'
+ 'rspec:windows: [name, 0/1] 0/1' | 'rspec:windows'
+ 'rspec:windows: [0/1, name]' | 'rspec:windows'
+ 'rspec:windows: [, ]' | 'rspec:windows'
+ 'rspec:windows: [name]' | 'rspec:windows'
+ 'rspec:windows: [name,other]' | 'rspec:windows'
+ end
+
+ with_them do
+ it "#{params[:name]} puts in #{params[:group_name]}" do
+ commit_status.name = name
+
+ is_expected.to eq(group_name)
+ end
+ end
+ end
end
describe '#detailed_status' do
@@ -725,22 +781,6 @@ RSpec.describe CommitStatus do
let(:commit_status) { create(:commit_status) }
it { is_expected.to eq(true) }
-
- context 'when build requires a resource' do
- before do
- allow(commit_status).to receive(:requires_resource?) { true }
- end
-
- it { is_expected.to eq(false) }
- end
-
- context 'when build has a prerequisite' do
- before do
- allow(commit_status).to receive(:any_unmet_prerequisites?) { true }
- end
-
- it { is_expected.to eq(false) }
- end
end
describe '#enqueue' do
@@ -748,7 +788,6 @@ RSpec.describe CommitStatus do
before do
allow(Time).to receive(:now).and_return(current_time)
- expect(commit_status.any_unmet_prerequisites?).to eq false
end
shared_examples 'commit status enqueued' do
diff --git a/spec/models/concerns/atomic_internal_id_spec.rb b/spec/models/concerns/atomic_internal_id_spec.rb
index 5ee3c012dc9..35b0f107676 100644
--- a/spec/models/concerns/atomic_internal_id_spec.rb
+++ b/spec/models/concerns/atomic_internal_id_spec.rb
@@ -87,6 +87,158 @@ RSpec.describe AtomicInternalId do
end
end
+ describe '#clear_scope_iid!' do
+ context 'when no ensure_if condition is given' do
+ it 'clears automatically set IIDs' do
+ expect(milestone).to receive(:clear_project_iid!).and_call_original
+
+ expect_iid_to_be_set_and_rollback(milestone)
+
+ expect(milestone.iid).to be_nil
+ end
+
+ it 'does not clear manually set IIDS' do
+ milestone.iid = external_iid
+
+ expect(milestone).to receive(:clear_project_iid!).and_call_original
+
+ expect_iid_to_be_set_and_rollback(milestone)
+
+ expect(milestone.iid).to eq(external_iid)
+ end
+ end
+
+ context 'when an ensure_if condition is given' do
+ let(:test_class) do
+ Class.new(ApplicationRecord) do
+ include AtomicInternalId
+ include Importable
+
+ self.table_name = :milestones
+
+ belongs_to :project
+
+ has_internal_id :iid, scope: :project, track_if: -> { !importing }, ensure_if: -> { !importing }
+
+ def self.name
+ 'TestClass'
+ end
+ end
+ end
+
+ let(:instance) { test_class.new(milestone.attributes) }
+
+ context 'when the ensure_if condition evaluates to true' do
+ it 'clears automatically set IIDs' do
+ expect(instance).to receive(:clear_project_iid!).and_call_original
+
+ expect_iid_to_be_set_and_rollback(instance)
+
+ expect(instance.iid).to be_nil
+ end
+
+ it 'does not clear manually set IIDs' do
+ instance.iid = external_iid
+
+ expect(instance).to receive(:clear_project_iid!).and_call_original
+
+ expect_iid_to_be_set_and_rollback(instance)
+
+ expect(instance.iid).to eq(external_iid)
+ end
+ end
+
+ context 'when the ensure_if condition evaluates to false' do
+ before do
+ instance.importing = true
+ end
+
+ it 'does not clear IIDs' do
+ instance.iid = external_iid
+
+ expect(instance).not_to receive(:clear_project_iid!)
+
+ expect_iid_to_be_set_and_rollback(instance)
+
+ expect(instance.iid).to eq(external_iid)
+ end
+ end
+ end
+
+ def expect_iid_to_be_set_and_rollback(instance)
+ ActiveRecord::Base.transaction(requires_new: true) do
+ instance.save!
+
+ expect(instance.iid).not_to be_nil
+
+ raise ActiveRecord::Rollback
+ end
+ end
+ end
+
+ describe '#validate_scope_iid_exists!' do
+ let(:test_class) do
+ Class.new(ApplicationRecord) do
+ include AtomicInternalId
+ include Importable
+
+ self.table_name = :milestones
+
+ belongs_to :project
+
+ def self.name
+ 'TestClass'
+ end
+ end
+ end
+
+ let(:instance) { test_class.new(milestone.attributes) }
+
+ before do
+ test_class.has_internal_id :iid, scope: :project, presence: presence, ensure_if: -> { !importing }
+
+ instance.importing = true
+ end
+
+ context 'when the presence flag is set' do
+ let(:presence) { true }
+
+ it 'raises an error for blank iids on create' do
+ expect do
+ instance.save!
+ end.to raise_error(described_class::MissingValueError, 'iid was unexpectedly blank!')
+ end
+
+ it 'raises an error for blank iids on update' do
+ instance.iid = 100
+ instance.save!
+
+ instance.iid = nil
+
+ expect do
+ instance.save!
+ end.to raise_error(described_class::MissingValueError, 'iid was unexpectedly blank!')
+ end
+ end
+
+ context 'when the presence flag is not set' do
+ let(:presence) { false }
+
+ it 'does not raise an error for blank iids on create' do
+ expect { instance.save! }.not_to raise_error
+ end
+
+ it 'does not raise an error for blank iids on update' do
+ instance.iid = 100
+ instance.save!
+
+ instance.iid = nil
+
+ expect { instance.save! }.not_to raise_error
+ end
+ end
+ end
+
describe '.with_project_iid_supply' do
let(:iid) { 100 }
diff --git a/spec/models/concerns/bulk_insert_safe_spec.rb b/spec/models/concerns/bulk_insert_safe_spec.rb
index 82b0c00b396..e40b0cf11ff 100644
--- a/spec/models/concerns/bulk_insert_safe_spec.rb
+++ b/spec/models/concerns/bulk_insert_safe_spec.rb
@@ -44,7 +44,6 @@ RSpec.describe BulkInsertSafe do
insecure_mode: false
default_value_for :enum_value, 'case_1'
- default_value_for :secret_value, 'my-secret'
default_value_for :sha_value, '2fd4e1c67a2d28fced849ee1bb76e7391b93eb12'
default_value_for :jsonb_value, { "key" => "value" }
@@ -53,11 +52,11 @@ RSpec.describe BulkInsertSafe do
end
def self.valid_list(count)
- Array.new(count) { |n| new(name: "item-#{n}") }
+ Array.new(count) { |n| new(name: "item-#{n}", secret_value: 'my-secret') }
end
def self.invalid_list(count)
- Array.new(count) { new }
+ Array.new(count) { new(secret_value: 'my-secret') }
end
end
end
@@ -102,7 +101,7 @@ RSpec.describe BulkInsertSafe do
context 'primary keys' do
it 'raises error if primary keys are set prior to insertion' do
- item = bulk_insert_item_class.new(name: 'valid', id: 10)
+ item = bulk_insert_item_class.new(name: 'valid', id: 10, secret_value: 'my-secret')
expect { bulk_insert_item_class.bulk_insert!([item]) }
.to raise_error(bulk_insert_item_class::PrimaryKeySetError)
diff --git a/spec/models/concerns/featurable_spec.rb b/spec/models/concerns/featurable_spec.rb
index 99acc563950..b550d22f686 100644
--- a/spec/models/concerns/featurable_spec.rb
+++ b/spec/models/concerns/featurable_spec.rb
@@ -134,22 +134,6 @@ RSpec.describe Featurable do
expect(project.feature_available?(:issues, user)).to eq(true)
end
end
-
- context 'when feature is disabled by a feature flag' do
- it 'returns false' do
- stub_feature_flags(issues: false)
-
- expect(project.feature_available?(:issues, user)).to eq(false)
- end
- end
-
- context 'when feature is enabled by a feature flag' do
- it 'returns true' do
- stub_feature_flags(issues: true)
-
- expect(project.feature_available?(:issues, user)).to eq(true)
- end
- end
end
describe '#*_enabled?' do
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index ff5b270cf33..3545c8e9686 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe Issuable do
include ProjectForksHelper
+ using RSpec::Parameterized::TableSyntax
let(:issuable_class) { Issue }
let(:issue) { create(:issue, title: 'An issue', description: 'A description') }
@@ -45,13 +46,17 @@ RSpec.describe Issuable do
end
it { is_expected.to validate_presence_of(:project) }
- it { is_expected.to validate_presence_of(:iid) }
it { is_expected.to validate_presence_of(:author) }
it { is_expected.to validate_presence_of(:title) }
it { is_expected.to validate_length_of(:title).is_at_most(described_class::TITLE_LENGTH_MAX) }
it { is_expected.to validate_length_of(:description).is_at_most(described_class::DESCRIPTION_LENGTH_MAX).on(:create) }
- it_behaves_like 'validates description length with custom validation'
+ it_behaves_like 'validates description length with custom validation' do
+ before do
+ allow(InternalId).to receive(:generate_next).and_call_original
+ end
+ end
+
it_behaves_like 'truncates the description to its allowed maximum length on import'
end
end
@@ -820,8 +825,6 @@ RSpec.describe Issuable do
end
describe '#supports_time_tracking?' do
- using RSpec::Parameterized::TableSyntax
-
where(:issuable_type, :supports_time_tracking) do
:issue | true
:incident | true
@@ -838,8 +841,6 @@ RSpec.describe Issuable do
end
describe '#supports_severity?' do
- using RSpec::Parameterized::TableSyntax
-
where(:issuable_type, :supports_severity) do
:issue | false
:incident | true
@@ -856,8 +857,6 @@ RSpec.describe Issuable do
end
describe '#incident?' do
- using RSpec::Parameterized::TableSyntax
-
where(:issuable_type, :incident) do
:issue | false
:incident | true
@@ -874,8 +873,6 @@ RSpec.describe Issuable do
end
describe '#supports_issue_type?' do
- using RSpec::Parameterized::TableSyntax
-
where(:issuable_type, :supports_issue_type) do
:issue | true
:merge_request | false
@@ -894,8 +891,6 @@ RSpec.describe Issuable do
subject { issuable.severity }
context 'when issuable is not an incident' do
- using RSpec::Parameterized::TableSyntax
-
where(:issuable_type, :severity) do
:issue | 'unknown'
:merge_request | 'unknown'
diff --git a/spec/models/concerns/nullify_if_blank_spec.rb b/spec/models/concerns/nullify_if_blank_spec.rb
new file mode 100644
index 00000000000..2d1bdba39dd
--- /dev/null
+++ b/spec/models/concerns/nullify_if_blank_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe NullifyIfBlank do
+ let_it_be(:model) do
+ Class.new(ApplicationRecord) do
+ include NullifyIfBlank
+
+ nullify_if_blank :name
+
+ self.table_name = 'users'
+ end
+ end
+
+ context 'attribute exists' do
+ let(:instance) { model.new(name: name) }
+
+ subject { instance.name }
+
+ before do
+ instance.validate
+ end
+
+ context 'attribute is blank' do
+ let(:name) { '' }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'attribute is nil' do
+ let(:name) { nil }
+
+ it { is_expected.to be_nil}
+ end
+
+ context 'attribute is not blank' do
+ let(:name) { 'name' }
+
+ it { is_expected.to eq('name') }
+ end
+ end
+
+ context 'attribute does not exist' do
+ before do
+ model.table_name = 'issues'
+ end
+
+ it { expect { model.new.valid? }.to raise_error(ActiveModel::UnknownAttributeError) }
+ end
+end
diff --git a/spec/models/concerns/protected_ref_spec.rb b/spec/models/concerns/protected_ref_spec.rb
new file mode 100644
index 00000000000..0a020736269
--- /dev/null
+++ b/spec/models/concerns/protected_ref_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ProtectedRef do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user, maintainer_projects: [project]) }
+
+ where(:klass, :factory, :action) do
+ ProtectedBranch | :protected_branch | :push
+ ProtectedTag | :protected_tag | :create
+ end
+
+ with_them do
+ describe '#protected_ref_accessible_to?' do
+ subject do
+ klass.protected_ref_accessible_to?('release', user, project: project, action: action)
+ end
+
+ it 'user cannot do action if rules do not exist' do
+ is_expected.to be_falsy
+ end
+
+ context 'the ref is protected' do
+ let!(:default_rule) { create(factory, :"developers_can_#{action}", project: project, name: 'release') }
+
+ context 'all rules permit action' do
+ let!(:maintainers_can) { create(factory, :"maintainers_can_#{action}", project: project, name: 'release*') }
+
+ it 'user can do action' do
+ is_expected.to be_truthy
+ end
+ end
+
+ context 'one of the rules forbids action' do
+ let!(:no_one_can) { create(factory, :"no_one_can_#{action}", project: project, name: 'release*') }
+
+ it 'user cannot do action' do
+ is_expected.to be_falsy
+ end
+ end
+ end
+ end
+
+ describe '#developers_can?' do
+ subject do
+ klass.developers_can?(action, 'release')
+ end
+
+ it 'developers cannot do action if rules do not exist' do
+ is_expected.to be_falsy
+ end
+
+ context 'the ref is protected' do
+ let!(:default_rule) { create(factory, :"developers_can_#{action}", project: project, name: 'release') }
+
+ context 'all rules permit developers to do action' do
+ let!(:developers_can) { create(factory, :"developers_can_#{action}", project: project, name: 'release*') }
+
+ it 'developers can do action' do
+ is_expected.to be_truthy
+ end
+ end
+
+ context 'one of the rules forbids developers to do action' do
+ let!(:maintainers_can) { create(factory, :"maintainers_can_#{action}", project: project, name: 'release*') }
+
+ it 'developers cannot do action' do
+ is_expected.to be_falsy
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/spammable_spec.rb b/spec/models/concerns/spammable_spec.rb
index d4fcb2e99eb..3c5f3b2d2ad 100644
--- a/spec/models/concerns/spammable_spec.rb
+++ b/spec/models/concerns/spammable_spec.rb
@@ -120,6 +120,61 @@ RSpec.describe Spammable do
end
end
+ describe '#render_recaptcha?' do
+ before do
+ allow(Gitlab::Recaptcha).to receive(:enabled?) { recaptcha_enabled }
+ end
+
+ context 'when recaptcha is not enabled' do
+ let(:recaptcha_enabled) { false }
+
+ it 'returns false' do
+ expect(issue.render_recaptcha?).to eq(false)
+ end
+ end
+
+ context 'when recaptcha is enabled' do
+ let(:recaptcha_enabled) { true }
+
+ context 'when there are two or more errors' do
+ before do
+ issue.errors.add(:base, 'a spam error')
+ issue.errors.add(:base, 'some other error')
+ end
+
+ it 'returns false' do
+ expect(issue.render_recaptcha?).to eq(false)
+ end
+ end
+
+ context 'when there are less than two errors' do
+ before do
+ issue.errors.add(:base, 'a spam error')
+ end
+
+ context 'when spammable does not need recaptcha' do
+ before do
+ issue.needs_recaptcha = false
+ end
+
+ it 'returns false' do
+ expect(issue.render_recaptcha?).to eq(false)
+ end
+ end
+
+ context 'when spammable needs recaptcha' do
+ before do
+ issue.needs_recaptcha!
+ end
+
+ it 'returns false' do
+ expect(issue.render_recaptcha?).to eq(true)
+ end
+ end
+ end
+ end
+ end
+
describe '#clear_spam_flags!' do
it 'clears spam and recaptcha flags' do
issue.spam = true
diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb
index d8b77e1cd0d..2df76684d71 100644
--- a/spec/models/concerns/token_authenticatable_spec.rb
+++ b/spec/models/concerns/token_authenticatable_spec.rb
@@ -54,7 +54,7 @@ RSpec.describe ApplicationSetting, 'TokenAuthenticatable' do
it 'persists new token as an encrypted string' do
expect(subject).to eq settings.reload.runners_registration_token
expect(settings.read_attribute('runners_registration_token_encrypted'))
- .to eq Gitlab::CryptoHelper.aes256_gcm_encrypt(subject)
+ .to eq Gitlab::CryptoHelper.aes256_gcm_encrypt(subject, nonce: Gitlab::CryptoHelper::AES256_GCM_IV_STATIC)
expect(settings).to be_persisted
end
@@ -243,7 +243,7 @@ RSpec.describe Ci::Build, 'TokenAuthenticatable' do
it 'persists new token as an encrypted string' do
build.ensure_token!
- encrypted = Gitlab::CryptoHelper.aes256_gcm_encrypt(build.token)
+ encrypted = Gitlab::CryptoHelper.aes256_gcm_encrypt(build.token, nonce: Gitlab::CryptoHelper::AES256_GCM_IV_STATIC)
expect(build.read_attribute('token_encrypted')).to eq encrypted
end
diff --git a/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb b/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb
index f6b8cf7def4..1e1cd97e410 100644
--- a/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb
+++ b/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb
@@ -68,6 +68,10 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
context 'when using optional strategy' do
let(:options) { { encrypted: :optional } }
+ before do
+ stub_feature_flags(dynamic_nonce_creation: false)
+ end
+
it 'returns decrypted token when an encrypted token is present' do
allow(instance).to receive(:read_attribute)
.with('some_field_encrypted')
@@ -124,7 +128,7 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
it 'writes encrypted token and removes plaintext token and returns it' do
expect(instance).to receive(:[]=)
- .with('some_field_encrypted', encrypted)
+ .with('some_field_encrypted', any_args)
expect(instance).to receive(:[]=)
.with('some_field', nil)
@@ -137,7 +141,7 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
it 'writes encrypted token and writes plaintext token' do
expect(instance).to receive(:[]=)
- .with('some_field_encrypted', encrypted)
+ .with('some_field_encrypted', any_args)
expect(instance).to receive(:[]=)
.with('some_field', 'my-value')
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index 5bc61db6d21..68d12f51d4b 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -254,26 +254,6 @@ RSpec.describe Deployment do
deployment.send(event)
end
-
- context 'the feature is disabled' do
- it 'does not trigger a worker' do
- stub_feature_flags(jira_sync_deployments: false)
-
- expect(worker).not_to receive(:perform_async)
-
- deployment.send(event)
- end
- end
-
- context 'the feature is enabled for this project' do
- it 'does trigger a worker' do
- stub_feature_flags(jira_sync_deployments: deployment.project)
-
- expect(worker).to receive(:perform_async)
-
- deployment.send(event)
- end
- end
end
end
end
@@ -416,6 +396,26 @@ RSpec.describe Deployment do
end
end
+ describe '.finished_before' do
+ let!(:deployment1) { create(:deployment, finished_at: 1.day.ago) }
+ let!(:deployment2) { create(:deployment, finished_at: Time.current) }
+
+ it 'filters deployments by finished_at' do
+ expect(described_class.finished_before(1.hour.ago))
+ .to eq([deployment1])
+ end
+ end
+
+ describe '.finished_after' do
+ let!(:deployment1) { create(:deployment, finished_at: 1.day.ago) }
+ let!(:deployment2) { create(:deployment, finished_at: Time.current) }
+
+ it 'filters deployments by finished_at' do
+ expect(described_class.finished_after(1.hour.ago))
+ .to eq([deployment2])
+ end
+ end
+
describe 'with_deployable' do
subject { described_class.with_deployable }
@@ -428,22 +428,6 @@ RSpec.describe Deployment do
end
end
- describe 'finished_between' do
- subject { described_class.finished_between(start_time, end_time) }
-
- let_it_be(:start_time) { DateTime.new(2017) }
- let_it_be(:end_time) { DateTime.new(2019) }
- let_it_be(:deployment_2016) { create(:deployment, finished_at: DateTime.new(2016)) }
- let_it_be(:deployment_2017) { create(:deployment, finished_at: DateTime.new(2017)) }
- let_it_be(:deployment_2018) { create(:deployment, finished_at: DateTime.new(2018)) }
- let_it_be(:deployment_2019) { create(:deployment, finished_at: DateTime.new(2019)) }
- let_it_be(:deployment_2020) { create(:deployment, finished_at: DateTime.new(2020)) }
-
- it 'retrieves deployments that finished between the specified times' do
- is_expected.to contain_exactly(deployment_2017, deployment_2018)
- end
- end
-
describe 'visible' do
subject { described_class.visible }
diff --git a/spec/models/design_management/design_spec.rb b/spec/models/design_management/design_spec.rb
index d3ce2f2d48f..674d2fc420d 100644
--- a/spec/models/design_management/design_spec.rb
+++ b/spec/models/design_management/design_spec.rb
@@ -629,25 +629,4 @@ RSpec.describe DesignManagement::Design do
end
end
end
-
- describe '#immediately_before' do
- let_it_be(:design) { create(:design, issue: issue, relative_position: 100) }
- let_it_be(:next_design) { create(:design, issue: issue, relative_position: 200) }
-
- it 'is true when there is no element positioned between this item and the next' do
- expect(design.immediately_before?(next_design)).to be true
- end
-
- it 'is false when there is an element positioned between this item and the next' do
- create(:design, issue: issue, relative_position: 150)
-
- expect(design.immediately_before?(next_design)).to be false
- end
-
- it 'is false when the next design is to the left of this design' do
- further_left = create(:design, issue: issue, relative_position: 50)
-
- expect(design.immediately_before?(further_left)).to be false
- end
- end
end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index 47492715c11..47148c4febc 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -744,13 +744,19 @@ RSpec.describe Event do
describe '#wiki_page and #wiki_page?' do
context 'for a wiki page event' do
- let(:wiki_page) do
- create(:wiki_page, project: project)
- end
+ let(:wiki_page) { create(:wiki_page, project: project) }
subject(:event) { create(:wiki_page_event, project: project, wiki_page: wiki_page) }
it { is_expected.to have_attributes(wiki_page?: be_truthy, wiki_page: wiki_page) }
+
+ context 'title is empty' do
+ before do
+ expect(event.target).to receive(:canonical_slug).and_return('')
+ end
+
+ it { is_expected.to have_attributes(wiki_page?: be_truthy, wiki_page: nil) }
+ end
end
context 'for any other event' do
@@ -907,6 +913,58 @@ RSpec.describe Event do
end
end
+ context 'with snippet note' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:note_on_project_snippet) { create(:note_on_project_snippet, author: user) }
+ let_it_be(:note_on_personal_snippet) { create(:note_on_personal_snippet, author: user) }
+ let_it_be(:other_note) { create(:note_on_issue, author: user)}
+ let_it_be(:personal_snippet_event) { create(:event, :commented, project: nil, target: note_on_personal_snippet, author: user) }
+ let_it_be(:project_snippet_event) { create(:event, :commented, project: note_on_project_snippet.project, target: note_on_project_snippet, author: user) }
+ let_it_be(:other_event) { create(:event, :commented, project: other_note.project, target: other_note, author: user) }
+
+ describe '#snippet_note?' do
+ it 'returns true for a project snippet event' do
+ expect(project_snippet_event.snippet_note?).to be true
+ end
+
+ it 'returns true for a personal snippet event' do
+ expect(personal_snippet_event.snippet_note?).to be true
+ end
+
+ it 'returns false for a other kinds of event' do
+ expect(other_event.snippet_note?).to be false
+ end
+ end
+
+ describe '#personal_snippet_note?' do
+ it 'returns false for a project snippet event' do
+ expect(project_snippet_event.personal_snippet_note?).to be false
+ end
+
+ it 'returns true for a personal snippet event' do
+ expect(personal_snippet_event.personal_snippet_note?).to be true
+ end
+
+ it 'returns false for a other kinds of event' do
+ expect(other_event.personal_snippet_note?).to be false
+ end
+ end
+
+ describe '#project_snippet_note?' do
+ it 'returns true for a project snippet event' do
+ expect(project_snippet_event.project_snippet_note?).to be true
+ end
+
+ it 'returns false for a personal snippet event' do
+ expect(personal_snippet_event.project_snippet_note?).to be false
+ end
+
+ it 'returns false for a other kinds of event' do
+ expect(other_event.project_snippet_note?).to be false
+ end
+ end
+ end
+
describe '#action_name' do
it 'handles all valid design events' do
created, updated, destroyed, archived = %i[created updated destroyed archived].map do |trait|
diff --git a/spec/models/experiment_spec.rb b/spec/models/experiment_spec.rb
index 171bfd116d3..22bbf2df8fd 100644
--- a/spec/models/experiment_spec.rb
+++ b/spec/models/experiment_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Experiment do
+ include AfterNextHelpers
+
subject { build(:experiment) }
describe 'associations' do
@@ -67,6 +69,33 @@ RSpec.describe Experiment do
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_group_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) }
@@ -136,6 +165,34 @@ RSpec.describe Experiment do
end
end
+ describe '#record_group_and_variant!' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:variant) { :control }
+ let_it_be(:experiment) { create(:experiment) }
+
+ subject(:record_group_and_variant!) { experiment.record_group_and_variant!(group, variant) }
+
+ context 'when no existing experiment_subject record exists for the given group' do
+ it 'creates an experiment_subject record' do
+ expect_next(ExperimentSubject).to receive(:update!).with(variant: variant).and_call_original
+
+ expect { record_group_and_variant! }.to change(ExperimentSubject, :count).by(1)
+ end
+ end
+
+ context 'when an existing experiment_subject exists for the given group' do
+ context 'but it belonged to a different variant' do
+ let!(:experiment_subject) do
+ create(:experiment_subject, experiment: experiment, group: group, user: nil, variant: :experimental)
+ end
+
+ it 'updates the variant value' do
+ expect { record_group_and_variant! }.to change { experiment_subject.reload.variant }.to('control')
+ end
+ end
+ end
+ end
+
describe '#record_user_and_group' do
let_it_be(:experiment) { create(:experiment) }
let_it_be(:user) { create(:user) }
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 0acf2b96b74..e79b54b4674 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -32,6 +32,7 @@ RSpec.describe Group do
it { is_expected.to have_many(:dependency_proxy_blobs) }
it { is_expected.to have_many(:dependency_proxy_manifests) }
it { is_expected.to have_many(:debian_distributions).class_name('Packages::Debian::GroupDistribution').dependent(:destroy) }
+ it { is_expected.to have_many(:daily_build_group_report_results).class_name('Ci::DailyBuildGroupReportResult') }
describe '#members & #requesters' do
let(:requester) { create(:user) }
@@ -410,7 +411,7 @@ RSpec.describe Group do
it "is false if avatar is html page" do
group.update_attribute(:avatar, 'uploads/avatar.html')
- expect(group.avatar_type).to eq(["file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico"])
+ expect(group.avatar_type).to eq(["file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico, webp"])
end
end
@@ -1492,6 +1493,28 @@ RSpec.describe Group do
end
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) }
+ let_it_be(:subgroup2, reload: true) { create(:group, parent: subgroup) }
+
+ it 'does noting for single group' do
+ expect(subgroup).not_to receive(:self_and_ancestors)
+
+ described_class.preset_root_ancestor_for([subgroup])
+ end
+
+ it 'sets the same root_ancestor for multiple groups' do
+ expect(subgroup).not_to receive(:self_and_ancestors)
+ expect(subgroup2).not_to receive(:self_and_ancestors)
+
+ described_class.preset_root_ancestor_for([rootgroup, subgroup, subgroup2])
+
+ expect(subgroup.root_ancestor).to eq(rootgroup)
+ expect(subgroup2.root_ancestor).to eq(rootgroup)
+ end
+ end
+
def subject_and_reload(*models)
subject
models.map(&:reload)
@@ -1756,19 +1779,6 @@ RSpec.describe Group do
describe 'with Debian Distributions' do
subject { create(:group) }
- let!(:distributions) { create_list(:debian_group_distribution, 2, :with_file, container: subject) }
-
- it 'removes distribution files on removal' do
- distribution_file_paths = distributions.map do |distribution|
- distribution.file.path
- end
-
- expect { subject.destroy }
- .to change {
- distribution_file_paths.select do |path|
- File.exist? path
- end.length
- }.from(distribution_file_paths.length).to(0)
- end
+ it_behaves_like 'model with Debian distributions'
end
end
diff --git a/spec/models/issue_link_spec.rb b/spec/models/issue_link_spec.rb
index ef41108ebea..433b51b8a70 100644
--- a/spec/models/issue_link_spec.rb
+++ b/spec/models/issue_link_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe IssueLink do
end
describe 'link_type' do
- it { is_expected.to define_enum_for(:link_type).with_values(relates_to: 0, blocks: 1, is_blocked_by: 2) }
+ it { is_expected.to define_enum_for(:link_type).with_values(relates_to: 0, blocks: 1) }
it 'provides the "related" as default link_type' do
expect(create(:issue_link).link_type).to eq 'relates_to'
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 81f045b4db1..969d897e551 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -1244,9 +1244,7 @@ RSpec.describe Issue do
end
describe '#allows_reviewers?' do
- it 'returns false as issues do not support reviewers feature' do
- stub_feature_flags(merge_request_reviewers: true)
-
+ it 'returns false as we do not support reviewers on issues yet' do
issue = build_stubbed(:issue)
expect(issue.allows_reviewers?).to be(false)
diff --git a/spec/models/license_template_spec.rb b/spec/models/license_template_spec.rb
index 515f728f515..fe06d1a357c 100644
--- a/spec/models/license_template_spec.rb
+++ b/spec/models/license_template_spec.rb
@@ -57,6 +57,6 @@ RSpec.describe LicenseTemplate do
end
def build_template(content)
- described_class.new(key: 'foo', name: 'foo', category: :Other, content: content)
+ described_class.new(key: 'foo', name: 'foo', project: nil, category: :Other, content: content)
end
end
diff --git a/spec/models/merge_request/metrics_spec.rb b/spec/models/merge_request/metrics_spec.rb
index 760eaf1ac7f..13ff239a306 100644
--- a/spec/models/merge_request/metrics_spec.rb
+++ b/spec/models/merge_request/metrics_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe MergeRequest::Metrics do
describe 'associations' do
it { is_expected.to belong_to(:merge_request) }
+ it { is_expected.to belong_to(:target_project).class_name('Project') }
it { is_expected.to belong_to(:latest_closed_by).class_name('User') }
it { is_expected.to belong_to(:merged_by).class_name('User') }
end
@@ -36,5 +37,15 @@ RSpec.describe MergeRequest::Metrics do
is_expected.not_to include([metrics_2])
end
end
+
+ describe '.by_target_project' do
+ let(:target_project) { metrics_1.target_project }
+
+ subject { described_class.by_target_project(target_project) }
+
+ it 'finds metrics record with the associated target project' do
+ is_expected.to eq([metrics_1])
+ end
+ end
end
end
diff --git a/spec/models/merge_request_diff_commit_spec.rb b/spec/models/merge_request_diff_commit_spec.rb
index 2edf44ecdc4..a24628b0f9d 100644
--- a/spec/models/merge_request_diff_commit_spec.rb
+++ b/spec/models/merge_request_diff_commit_spec.rb
@@ -48,7 +48,8 @@ RSpec.describe MergeRequestDiffCommit do
"committer_email": "dmitriy.zaporozhets@gmail.com",
"merge_request_diff_id": merge_request_diff_id,
"relative_order": 0,
- "sha": Gitlab::Database::ShaAttribute.serialize("5937ac0a7beb003549fc5fd26fc247adbce4a52e")
+ "sha": Gitlab::Database::ShaAttribute.serialize("5937ac0a7beb003549fc5fd26fc247adbce4a52e"),
+ "trailers": {}.to_json
},
{
"message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
@@ -60,7 +61,8 @@ RSpec.describe MergeRequestDiffCommit do
"committer_email": "dmitriy.zaporozhets@gmail.com",
"merge_request_diff_id": merge_request_diff_id,
"relative_order": 1,
- "sha": Gitlab::Database::ShaAttribute.serialize("570e7b2abdd848b95f2f578043fc23bd6f6fd24d")
+ "sha": Gitlab::Database::ShaAttribute.serialize("570e7b2abdd848b95f2f578043fc23bd6f6fd24d"),
+ "trailers": {}.to_json
}
]
end
@@ -92,7 +94,8 @@ RSpec.describe MergeRequestDiffCommit do
"committer_email": "alejorro70@gmail.com",
"merge_request_diff_id": merge_request_diff_id,
"relative_order": 0,
- "sha": Gitlab::Database::ShaAttribute.serialize("ba3343bc4fa403a8dfbfcab7fc1a8c29ee34bd69")
+ "sha": Gitlab::Database::ShaAttribute.serialize("ba3343bc4fa403a8dfbfcab7fc1a8c29ee34bd69"),
+ "trailers": {}.to_json
}]
end
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index a5493d1650b..5b11a7bf079 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -16,6 +16,8 @@ RSpec.describe MergeRequestDiff do
describe 'validations' do
subject { diff_with_commits }
+ it { is_expected.not_to validate_uniqueness_of(:diff_type).scoped_to(:merge_request_id) }
+
it 'checks sha format of base_commit_sha, head_commit_sha and start_commit_sha' do
subject.base_commit_sha = subject.head_commit_sha = subject.start_commit_sha = 'foobar'
@@ -23,6 +25,24 @@ RSpec.describe MergeRequestDiff do
expect(subject.errors.count).to eq 3
expect(subject.errors).to all(include('is not a valid SHA'))
end
+
+ it 'does not validate uniqueness by default' do
+ expect(build(:merge_request_diff, merge_request: subject.merge_request)).to be_valid
+ end
+
+ context 'when merge request diff is a merge_head type' do
+ it 'is valid' do
+ expect(build(:merge_request_diff, :merge_head, merge_request: subject.merge_request)).to be_valid
+ end
+
+ context 'when merge_head diff exists' do
+ let(:existing_merge_head_diff) { create(:merge_request_diff, :merge_head) }
+
+ it 'validates uniqueness' do
+ expect(build(:merge_request_diff, :merge_head, merge_request: existing_merge_head_diff.merge_request)).not_to be_valid
+ end
+ end
+ end
end
describe 'create new record' do
@@ -35,12 +55,32 @@ RSpec.describe MergeRequestDiff do
it { expect(subject.head_commit_sha).to eq('b83d6e391c22777fca1ed3012fce84f633d7fed0') }
it { expect(subject.base_commit_sha).to eq('ae73cb07c9eeaf35924a10f713b364d32b2dd34f') }
it { expect(subject.start_commit_sha).to eq('0b4bc9a49b562e85de7cc9e834518ea6828729b9') }
+
+ context 'when diff_type is merge_head' do
+ let_it_be(:merge_request) { create(:merge_request) }
+
+ let_it_be(:merge_head) do
+ MergeRequests::MergeToRefService
+ .new(merge_request.project, merge_request.author)
+ .execute(merge_request)
+
+ merge_request.create_merge_head_diff
+ end
+
+ it { expect(merge_head).to be_valid }
+ it { expect(merge_head).to be_persisted }
+ it { expect(merge_head.commits.count).to eq(30) }
+ it { expect(merge_head.diffs.count).to eq(20) }
+ it { expect(merge_head.head_commit_sha).to eq(merge_request.merge_ref_head.diff_refs.head_sha) }
+ it { expect(merge_head.base_commit_sha).to eq(merge_request.merge_ref_head.diff_refs.base_sha) }
+ it { expect(merge_head.start_commit_sha).to eq(merge_request.target_branch_sha) }
+ end
end
describe '.by_commit_sha' do
subject(:by_commit_sha) { described_class.by_commit_sha(sha) }
- let!(:merge_request) { create(:merge_request, :with_diffs) }
+ let!(:merge_request) { create(:merge_request) }
context 'with sha contained in' do
let(:sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
@@ -63,6 +103,7 @@ RSpec.describe MergeRequestDiff do
let_it_be(:merge_request) { create(:merge_request) }
let_it_be(:outdated) { merge_request.merge_request_diff }
let_it_be(:latest) { merge_request.create_merge_request_diff }
+ let_it_be(:merge_head) { merge_request.create_merge_head_diff }
let_it_be(:closed_mr) { create(:merge_request, :closed_last_month) }
let(:closed) { closed_mr.merge_request_diff }
@@ -103,14 +144,14 @@ RSpec.describe MergeRequestDiff do
stub_external_diffs_setting(enabled: true)
end
- it { is_expected.to contain_exactly(outdated.id, latest.id, closed.id, merged.id, closed_recently.id, merged_recently.id) }
+ it { is_expected.to contain_exactly(outdated.id, latest.id, closed.id, merged.id, closed_recently.id, merged_recently.id, merge_head.id) }
it 'ignores diffs with 0 files' do
MergeRequestDiffFile.where(merge_request_diff_id: [closed_recently.id, merged_recently.id]).delete_all
closed_recently.update!(files_count: 0)
merged_recently.update!(files_count: 0)
- is_expected.to contain_exactly(outdated.id, latest.id, closed.id, merged.id)
+ is_expected.to contain_exactly(outdated.id, latest.id, closed.id, merged.id, merge_head.id)
end
end
@@ -317,7 +358,7 @@ RSpec.describe MergeRequestDiff do
end
describe '#latest?' do
- let!(:mr) { create(:merge_request, :with_diffs) }
+ let!(:mr) { create(:merge_request) }
let!(:first_diff) { mr.merge_request_diff }
let!(:last_diff) { mr.create_merge_request_diff }
@@ -326,7 +367,7 @@ RSpec.describe MergeRequestDiff do
end
shared_examples_for 'merge request diffs' do
- let(:merge_request) { create(:merge_request, :with_diffs) }
+ let(:merge_request) { create(:merge_request) }
let!(:diff) { merge_request.merge_request_diff.reload }
context 'when it was not cleaned by the system' do
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 1cf197322f5..ebe2cd2ac03 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -271,8 +271,6 @@ RSpec.describe MergeRequest, factory_default: :keep do
stub_feature_flags(stricter_mr_branch_name: false)
end
- using RSpec::Parameterized::TableSyntax
-
where(:branch_name, :valid) do
'foo' | true
'foo:bar' | false
@@ -367,7 +365,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
describe '.by_commit_sha' do
subject(:by_commit_sha) { described_class.by_commit_sha(sha) }
- let!(:merge_request) { create(:merge_request, :with_diffs) }
+ let!(:merge_request) { create(:merge_request) }
context 'with sha contained in latest merge request diff' do
let(:sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
@@ -433,7 +431,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
end
context 'when commit is a part of the merge request' do
- let!(:merge_request) { create(:merge_request, :with_diffs) }
+ let!(:merge_request) { create(:merge_request) }
let(:sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
it { is_expected.to eq([merge_request]) }
@@ -451,6 +449,17 @@ RSpec.describe MergeRequest, factory_default: :keep do
it { is_expected.to be_empty }
end
+
+ context 'when commit is part of the merge request and a squash commit at the same time' do
+ let!(:merge_request) { create(:merge_request) }
+ let(:sha) { merge_request.commits.first.id }
+
+ before do
+ merge_request.update!(squash_commit_sha: sha)
+ end
+
+ it { is_expected.to eq([merge_request]) }
+ end
end
describe '.by_cherry_pick_sha' do
@@ -480,6 +489,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
create(:merge_request, params).tap do |mr|
diffs.times { mr.merge_request_diffs.create }
+ mr.create_merge_head_diff
end
end
@@ -705,6 +715,10 @@ RSpec.describe MergeRequest, factory_default: :keep do
end
context 'when external issue tracker is enabled' do
+ let(:project) { create(:project, :repository) }
+
+ subject { create(:merge_request, source_project: project) }
+
before do
subject.project.has_external_issue_tracker = true
subject.project.save!
@@ -754,9 +768,8 @@ RSpec.describe MergeRequest, factory_default: :keep do
context 'when both internal and external issue trackers are enabled' do
before do
- subject.project.has_external_issue_tracker = true
- subject.project.save!
create(:jira_service, project: subject.project)
+ subject.project.reload
end
it 'does not cache issues from external trackers' do
@@ -779,6 +792,10 @@ RSpec.describe MergeRequest, factory_default: :keep do
end
context 'when only external issue tracker enabled' do
+ let(:project) { create(:project, :repository) }
+
+ subject { create(:merge_request, source_project: project) }
+
before do
subject.project.has_external_issue_tracker = true
subject.project.issues_enabled = false
@@ -808,7 +825,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
let(:last_branch_commit) { subject.source_project.repository.commit(Gitlab::Git::BRANCH_REF_PREFIX + subject.source_branch) }
context 'with diffs' do
- subject { create(:merge_request, :with_diffs) }
+ subject { create(:merge_request) }
it 'returns the sha of the source branch last commit' do
expect(subject.source_branch_sha).to eq(last_branch_commit.sha)
@@ -875,7 +892,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
let(:options) { { paths: ['a/b', 'b/a', 'c/*'] } }
context 'when there are MR diffs' do
- let(:merge_request) { create(:merge_request, :with_diffs) }
+ let(:merge_request) { create(:merge_request) }
it 'delegates to the MR diffs' do
expect(merge_request.merge_request_diff).to receive(:raw_diffs).with(options)
@@ -924,7 +941,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
describe '#note_positions_for_paths' do
let(:user) { create(:user) }
- let(:merge_request) { create(:merge_request, :with_diffs) }
+ let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
let!(:diff_note) do
create(:diff_note_on_merge_request, project: project, noteable: merge_request)
@@ -1263,7 +1280,6 @@ RSpec.describe MergeRequest, factory_default: :keep do
describe '#issues_mentioned_but_not_closing' do
let(:closing_issue) { create :issue, project: subject.project }
let(:mentioned_issue) { create :issue, project: subject.project }
-
let(:commit) { double('commit', safe_message: "Fixes #{closing_issue.to_reference}") }
it 'detects issues mentioned in description but not closed' do
@@ -1279,13 +1295,12 @@ RSpec.describe MergeRequest, factory_default: :keep do
end
context 'when the project has an external issue tracker' do
- subject { create(:merge_request, source_project: create(:project, :repository)) }
-
before do
subject.project.add_developer(subject.author)
commit = double(:commit, safe_message: 'Fixes TEST-3')
create(:jira_service, project: subject.project)
+ subject.project.reload
allow(subject).to receive(:commits).and_return([commit])
allow(subject).to receive(:description).and_return('Is related to TEST-2 and TEST-3')
@@ -1645,7 +1660,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
end
it_behaves_like 'an editable mentionable' do
- subject { create(:merge_request, :simple) }
+ subject { create(:merge_request, :simple, source_project: create(:project, :repository)) }
let(:backref_text) { "merge request #{subject.to_reference}" }
let(:set_mentionable_text) { ->(txt) { subject.description = txt } }
@@ -1971,6 +1986,30 @@ RSpec.describe MergeRequest, factory_default: :keep do
end
end
+ describe '#has_codequality_mr_diff_report?' do
+ subject { merge_request.has_codequality_mr_diff_report? }
+
+ context 'when head pipeline has codequality mr diff report' do
+ let(:merge_request) { create(:merge_request, :with_codequality_mr_diff_reports) }
+
+ it { is_expected.to be_truthy }
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(codequality_mr_diff: false)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context 'when head pipeline does not have codeqquality mr diff report' do
+ let(:merge_request) { create(:merge_request) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
describe '#has_codequality_reports?' do
subject { merge_request.has_codequality_reports? }
@@ -1983,7 +2022,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
context 'when feature flag is disabled' do
before do
- stub_feature_flags(codequality_mr_diff: false)
+ stub_feature_flags(codequality_backend_comparison: false)
end
it { is_expected.to be_falsey }
@@ -2015,6 +2054,50 @@ RSpec.describe MergeRequest, factory_default: :keep do
end
end
+ describe '#has_sast_reports?' do
+ subject { merge_request.has_sast_reports? }
+
+ let(:project) { create(:project, :repository) }
+
+ before do
+ stub_licensed_features(sast: true)
+ end
+
+ context 'when head pipeline has sast reports' do
+ let(:merge_request) { create(:merge_request, :with_sast_reports, source_project: project) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when head pipeline does not have sast reports' do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#has_secret_detection_reports?' do
+ subject { merge_request.has_secret_detection_reports? }
+
+ let(:project) { create(:project, :repository) }
+
+ before do
+ stub_licensed_features(secret_detection: true)
+ end
+
+ context 'when head pipeline has secret detection reports' do
+ let(:merge_request) { create(:merge_request, :with_secret_detection_reports, source_project: project) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when head pipeline does not have secrets detection reports' do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
describe '#calculate_reactive_cache' do
let(:merge_request) { create(:merge_request) }
@@ -2144,6 +2227,54 @@ RSpec.describe MergeRequest, factory_default: :keep do
end
end
+ describe '#find_codequality_mr_diff_reports' do
+ let(:project) { create(:project, :repository) }
+ let(:merge_request) { create(:merge_request, :with_codequality_mr_diff_reports, source_project: project) }
+ let(:pipeline) { merge_request.head_pipeline }
+
+ subject(:mr_diff_report) { merge_request.find_codequality_mr_diff_reports }
+
+ context 'when head pipeline has coverage reports' do
+ context 'when reactive cache worker is parsing results asynchronously' do
+ it 'returns status' do
+ expect(mr_diff_report[:status]).to eq(:parsing)
+ end
+ end
+
+ context 'when reactive cache worker is inline' do
+ before do
+ synchronous_reactive_cache(merge_request)
+ end
+
+ it 'returns status and data' do
+ expect(mr_diff_report[:status]).to eq(:parsed)
+ end
+
+ context 'when an error occurrs' do
+ before do
+ merge_request.update!(head_pipeline: nil)
+ end
+
+ it 'returns an error message' do
+ expect(mr_diff_report[:status]).to eq(:error)
+ end
+ end
+
+ context 'when cached results is not latest' do
+ before do
+ allow_next_instance_of(Ci::GenerateCodequalityMrDiffReportService) do |service|
+ allow(service).to receive(:latest?).and_return(false)
+ end
+ end
+
+ it 'raises and InvalidateReactiveCache error' do
+ expect { mr_diff_report }.to raise_error(ReactiveCaching::InvalidateReactiveCache)
+ end
+ end
+ end
+ end
+ end
+
describe '#compare_test_reports' do
subject { merge_request.compare_test_reports }
@@ -2765,8 +2896,6 @@ RSpec.describe MergeRequest, factory_default: :keep do
end
context 'with skip_ci_check option' do
- using RSpec::Parameterized::TableSyntax
-
before do
allow(subject).to receive_messages(check_mergeability: nil,
can_be_merged?: true,
@@ -2790,8 +2919,6 @@ RSpec.describe MergeRequest, factory_default: :keep do
end
context 'with skip_discussions_check option' do
- using RSpec::Parameterized::TableSyntax
-
before do
allow(subject).to receive_messages(mergeable_ci_state?: true,
check_mergeability: nil,
@@ -3345,6 +3472,10 @@ RSpec.describe MergeRequest, factory_default: :keep do
end
context 'when resolve_outdated_diff_discussions is set' do
+ let(:project) { create(:project, :repository) }
+
+ subject { create(:merge_request, source_project: project) }
+
before do
discussion
@@ -3365,7 +3496,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
describe '#branch_merge_base_commit' do
let(:project) { create(:project, :repository) }
- subject { create(:merge_request, :with_diffs, source_project: project) }
+ subject { create(:merge_request, source_project: project) }
context 'source and target branch exist' do
it { expect(subject.branch_merge_base_commit.sha).to eq('ae73cb07c9eeaf35924a10f713b364d32b2dd34f') }
@@ -3388,7 +3519,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
context "with diffs" do
let(:project) { create(:project, :repository) }
- subject { create(:merge_request, :with_diffs, source_project: project) }
+ subject { create(:merge_request, source_project: project) }
let(:expected_diff_refs) do
Gitlab::Diff::DiffRefs.new(
@@ -3792,7 +3923,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
describe '#fetch_ref!' do
let(:project) { create(:project, :repository) }
- subject { create(:merge_request, :with_diffs, source_project: project) }
+ subject { create(:merge_request, source_project: project) }
it 'fetches the ref correctly' do
expect { subject.target_project.repository.delete_refs(subject.ref_path) }.not_to raise_error
@@ -4367,37 +4498,41 @@ RSpec.describe MergeRequest, factory_default: :keep do
end
describe '#diffable_merge_ref?' do
+ let(:merge_request) { create(:merge_request) }
+
context 'merge request can be merged' do
- context 'merge_to_ref is not calculated' do
+ context 'merge_head diff is not created' do
it 'returns true' do
- expect(subject.diffable_merge_ref?).to eq(false)
+ expect(merge_request.diffable_merge_ref?).to eq(false)
end
end
- context 'merge_to_ref is calculated' do
+ context 'merge_head diff is created' do
before do
- MergeRequests::MergeToRefService.new(subject.project, subject.author).execute(subject)
+ create(:merge_request_diff, :merge_head, merge_request: merge_request)
end
it 'returns true' do
- expect(subject.diffable_merge_ref?).to eq(true)
+ expect(merge_request.diffable_merge_ref?).to eq(true)
end
context 'merge request is merged' do
- subject { build_stubbed(:merge_request, :merged, project: project) }
+ before do
+ merge_request.mark_as_merged!
+ end
it 'returns false' do
- expect(subject.diffable_merge_ref?).to eq(false)
+ expect(merge_request.diffable_merge_ref?).to eq(false)
end
end
context 'merge request cannot be merged' do
before do
- subject.mark_as_unchecked!
+ merge_request.mark_as_unchecked!
end
it 'returns false' do
- expect(subject.diffable_merge_ref?).to eq(true)
+ expect(merge_request.diffable_merge_ref?).to eq(true)
end
context 'display_merge_conflicts_in_diff is disabled' do
@@ -4406,7 +4541,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
end
it 'returns false' do
- expect(subject.diffable_merge_ref?).to eq(false)
+ expect(merge_request.diffable_merge_ref?).to eq(false)
end
end
end
@@ -4476,17 +4611,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
end
describe '#allows_reviewers?' do
- it 'returns false without merge_request_reviewers feature' do
- stub_feature_flags(merge_request_reviewers: false)
-
- merge_request = build_stubbed(:merge_request)
-
- expect(merge_request.allows_reviewers?).to be(false)
- end
-
- it 'returns true with merge_request_reviewers feature' do
- stub_feature_flags(merge_request_reviewers: true)
-
+ it 'returns true' do
merge_request = build_stubbed(:merge_request)
expect(merge_request.allows_reviewers?).to be(true)
@@ -4506,4 +4631,34 @@ RSpec.describe MergeRequest, factory_default: :keep do
.from(nil).to(ref)
end
end
+
+ describe '#enabled_reports' do
+ let(:project) { create(:project, :repository) }
+
+ where(:report_type, :with_reports, :feature) do
+ :sast | :with_sast_reports | :sast
+ :secret_detection | :with_secret_detection_reports | :secret_detection
+ end
+
+ with_them do
+ subject { merge_request.enabled_reports[report_type] }
+
+ before do
+ stub_feature_flags(drop_license_management_artifact: false)
+ stub_licensed_features({ feature => true })
+ end
+
+ context "when head pipeline has reports" do
+ let(:merge_request) { create(:merge_request, with_reports, source_project: project) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context "when head pipeline does not have reports" do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ it { is_expected.to be_falsy }
+ end
+ end
+ end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index a3c0a43115e..647e279bf83 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -113,6 +113,7 @@ RSpec.describe Namespace do
describe 'inclusions' do
it { is_expected.to include_module(Gitlab::VisibilityLevel) }
+ it { is_expected.to include_module(Namespaces::Traversal::Recursive) }
end
describe '#visibility_level_field' do
@@ -770,80 +771,7 @@ RSpec.describe Namespace do
end
end
- describe '#self_and_hierarchy' do
- let!(:group) { create(:group, path: 'git_lab') }
- let!(:nested_group) { create(:group, parent: group) }
- let!(:deep_nested_group) { create(:group, parent: nested_group) }
- let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
- let!(:another_group) { create(:group, path: 'gitllab') }
- let!(:another_group_nested) { create(:group, path: 'foo', parent: another_group) }
-
- it 'returns the correct tree' do
- expect(group.self_and_hierarchy).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group)
- expect(nested_group.self_and_hierarchy).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group)
- expect(very_deep_nested_group.self_and_hierarchy).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group)
- end
- end
-
- describe '#ancestors' do
- let(:group) { create(:group) }
- let(:nested_group) { create(:group, parent: group) }
- let(:deep_nested_group) { create(:group, parent: nested_group) }
- let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
-
- it 'returns the correct ancestors' do
- expect(very_deep_nested_group.ancestors).to include(group, nested_group, deep_nested_group)
- expect(deep_nested_group.ancestors).to include(group, nested_group)
- expect(nested_group.ancestors).to include(group)
- expect(group.ancestors).to eq([])
- end
- end
-
- describe '#self_and_ancestors' do
- let(:group) { create(:group) }
- let(:nested_group) { create(:group, parent: group) }
- let(:deep_nested_group) { create(:group, parent: nested_group) }
- let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
-
- it 'returns the correct ancestors' do
- expect(very_deep_nested_group.self_and_ancestors).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group)
- expect(deep_nested_group.self_and_ancestors).to contain_exactly(group, nested_group, deep_nested_group)
- expect(nested_group.self_and_ancestors).to contain_exactly(group, nested_group)
- expect(group.self_and_ancestors).to contain_exactly(group)
- end
- end
-
- describe '#descendants' do
- let!(:group) { create(:group, path: 'git_lab') }
- let!(:nested_group) { create(:group, parent: group) }
- let!(:deep_nested_group) { create(:group, parent: nested_group) }
- let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
- let!(:another_group) { create(:group, path: 'gitllab') }
- let!(:another_group_nested) { create(:group, path: 'foo', parent: another_group) }
-
- it 'returns the correct descendants' do
- expect(very_deep_nested_group.descendants.to_a).to eq([])
- expect(deep_nested_group.descendants.to_a).to include(very_deep_nested_group)
- expect(nested_group.descendants.to_a).to include(deep_nested_group, very_deep_nested_group)
- expect(group.descendants.to_a).to include(nested_group, deep_nested_group, very_deep_nested_group)
- end
- end
-
- describe '#self_and_descendants' do
- let!(:group) { create(:group, path: 'git_lab') }
- let!(:nested_group) { create(:group, parent: group) }
- let!(:deep_nested_group) { create(:group, parent: nested_group) }
- let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
- let!(:another_group) { create(:group, path: 'gitllab') }
- let!(:another_group_nested) { create(:group, path: 'foo', parent: another_group) }
-
- it 'returns the correct descendants' do
- expect(very_deep_nested_group.self_and_descendants).to contain_exactly(very_deep_nested_group)
- expect(deep_nested_group.self_and_descendants).to contain_exactly(deep_nested_group, very_deep_nested_group)
- expect(nested_group.self_and_descendants).to contain_exactly(nested_group, deep_nested_group, very_deep_nested_group)
- expect(group.self_and_descendants).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group)
- end
- end
+ it_behaves_like 'recursive namespace traversal'
describe '#users_with_descendants' do
let(:user_a) { create(:user) }
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 6e87ca6dcf7..364b80e8601 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -837,6 +837,16 @@ RSpec.describe Note do
end
end
+ describe '#for_project_snippet?' do
+ it 'returns true for a project snippet note' do
+ expect(build(:note_on_project_snippet).for_project_snippet?).to be true
+ end
+
+ it 'returns false for a personal snippet note' do
+ expect(build(:note_on_personal_snippet).for_project_snippet?).to be false
+ end
+ end
+
describe '#for_personal_snippet?' do
it 'returns false for a project snippet note' do
expect(build(:note_on_project_snippet).for_personal_snippet?).to be_falsy
@@ -890,35 +900,31 @@ RSpec.describe Note do
describe '#cache_markdown_field' do
let(:html) { '<p>some html</p>'}
+ before do
+ allow(Banzai::Renderer).to receive(:cacheless_render_field).and_call_original
+ end
+
context 'note for a project snippet' do
let(:snippet) { create(:project_snippet) }
- let(:note) { build(:note_on_project_snippet, project: snippet.project, noteable: snippet) }
+ let(:note) { create(:note_on_project_snippet, project: snippet.project, noteable: snippet) }
- before do
+ it 'skips project check' do
expect(Banzai::Renderer).to receive(:cacheless_render_field)
- .with(note, :note, { skip_project_check: false }).and_return(html)
-
- note.save
- end
+ .with(note, :note, { skip_project_check: false })
- it 'creates a note' do
- expect(note.note_html).to eq(html)
+ note.update!(note: html)
end
end
context 'note for a personal snippet' do
let(:snippet) { create(:personal_snippet) }
- let(:note) { build(:note_on_personal_snippet, noteable: snippet) }
+ let(:note) { create(:note_on_personal_snippet, noteable: snippet) }
- before do
+ it 'does not skip project check' do
expect(Banzai::Renderer).to receive(:cacheless_render_field)
- .with(note, :note, { skip_project_check: true }).and_return(html)
-
- note.save
- end
+ .with(note, :note, { skip_project_check: true })
- it 'creates a note' do
- expect(note.note_html).to eq(html)
+ note.update!(note: html)
end
end
end
diff --git a/spec/models/onboarding_progress_spec.rb b/spec/models/onboarding_progress_spec.rb
index bd951846bb8..0aa19345a25 100644
--- a/spec/models/onboarding_progress_spec.rb
+++ b/spec/models/onboarding_progress_spec.rb
@@ -29,6 +29,67 @@ RSpec.describe OnboardingProgress do
end
end
+ describe 'scopes' do
+ describe '.incomplete_actions' do
+ subject { described_class.incomplete_actions(actions) }
+
+ let!(:no_actions_completed) { create(:onboarding_progress) }
+ let!(:one_action_completed_one_action_incompleted) { create(:onboarding_progress, "#{action}_at" => Time.current) }
+
+ context 'when given one action' do
+ let(:actions) { action }
+
+ it { is_expected.to eq [no_actions_completed] }
+ end
+
+ context 'when given an array of actions' do
+ let(:actions) { [action, :git_write] }
+
+ it { is_expected.to eq [no_actions_completed] }
+ end
+ end
+
+ describe '.completed_actions' do
+ subject { described_class.completed_actions(actions) }
+
+ let!(:one_action_completed_one_action_incompleted) { create(:onboarding_progress, "#{action}_at" => Time.current) }
+ let!(:both_actions_completed) { create(:onboarding_progress, "#{action}_at" => Time.current, git_write_at: Time.current) }
+
+ context 'when given one action' do
+ let(:actions) { action }
+
+ it { is_expected.to eq [one_action_completed_one_action_incompleted, both_actions_completed] }
+ end
+
+ context 'when given an array of actions' do
+ let(:actions) { [action, :git_write] }
+
+ it { is_expected.to eq [both_actions_completed] }
+ end
+ end
+
+ describe '.completed_actions_with_latest_in_range' do
+ subject { described_class.completed_actions_with_latest_in_range(actions, 1.day.ago.beginning_of_day..1.day.ago.end_of_day) }
+
+ let!(:one_action_completed_in_range_one_action_incompleted) { create(:onboarding_progress, "#{action}_at" => 1.day.ago.middle_of_day) }
+ let!(:git_write_action_completed_in_range) { create(:onboarding_progress, git_write_at: 1.day.ago.middle_of_day) }
+ let!(:both_actions_completed_latest_action_out_of_range) { create(:onboarding_progress, "#{action}_at" => 1.day.ago.middle_of_day, git_write_at: Time.current) }
+ let!(:both_actions_completed_latest_action_in_range) { create(:onboarding_progress, "#{action}_at" => 1.day.ago.middle_of_day, git_write_at: 2.days.ago.middle_of_day) }
+
+ context 'when given one action' do
+ let(:actions) { :git_write }
+
+ it { is_expected.to eq [git_write_action_completed_in_range] }
+ end
+
+ context 'when given an array of actions' do
+ let(:actions) { [action, :git_write] }
+
+ it { is_expected.to eq [both_actions_completed_latest_action_in_range] }
+ end
+ end
+ end
+
describe '.onboard' do
subject(:onboard) { described_class.onboard(namespace) }
@@ -53,6 +114,22 @@ RSpec.describe OnboardingProgress do
end
end
+ describe '.onboarding?' do
+ subject(:onboarding?) { described_class.onboarding?(namespace) }
+
+ context 'when onboarded' do
+ before do
+ described_class.onboard(namespace)
+ end
+
+ it { is_expected.to eq true }
+ end
+
+ context 'when not onboarding' do
+ it { is_expected.to eq false }
+ end
+ end
+
describe '.register' do
subject(:register_action) { described_class.register(namespace, action) }
@@ -104,4 +181,10 @@ RSpec.describe OnboardingProgress do
end
end
end
+
+ describe '.column_name' do
+ subject { described_class.column_name(action) }
+
+ it { is_expected.to eq(:subscription_created_at) }
+ end
end
diff --git a/spec/models/operations/feature_flag_spec.rb b/spec/models/operations/feature_flag_spec.rb
index 93dd7d4f0bb..d5b3c7a8582 100644
--- a/spec/models/operations/feature_flag_spec.rb
+++ b/spec/models/operations/feature_flag_spec.rb
@@ -16,6 +16,35 @@ RSpec.describe Operations::FeatureFlag do
it { is_expected.to have_many(:scopes) }
end
+ describe '.reference_pattern' do
+ subject { described_class.reference_pattern }
+
+ it { is_expected.to match('[feature_flag:123]') }
+ it { is_expected.to match('[feature_flag:gitlab-org/gitlab/123]') }
+ end
+
+ describe '.link_reference_pattern' do
+ subject { described_class.link_reference_pattern }
+
+ it { is_expected.to match("#{Gitlab.config.gitlab.url}/gitlab-org/gitlab/-/feature_flags/123/edit") }
+ it { is_expected.not_to match("#{Gitlab.config.gitlab.url}/gitlab-org/gitlab/issues/123/edit") }
+ it { is_expected.not_to match("gitlab-org/gitlab/-/feature_flags/123/edit") }
+ end
+
+ describe '#to_reference' do
+ let(:namespace) { build(:namespace, path: 'sample-namespace') }
+ let(:project) { build(:project, name: 'sample-project', namespace: namespace) }
+ let(:feature_flag) { build(:operations_feature_flag, iid: 1, project: project) }
+
+ it 'returns feature flag id' do
+ expect(feature_flag.to_reference).to eq '[feature_flag:1]'
+ end
+
+ it 'returns complete path to the feature flag with full: true' do
+ expect(feature_flag.to_reference(full: true)).to eq '[feature_flag:sample-namespace/sample-project/1]'
+ end
+ end
+
describe 'validations' do
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:name) }
diff --git a/spec/models/packages/composer/cache_file_spec.rb b/spec/models/packages/composer/cache_file_spec.rb
new file mode 100644
index 00000000000..a03b89ca2f5
--- /dev/null
+++ b/spec/models/packages/composer/cache_file_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::Composer::CacheFile, type: :model do
+ describe 'relationships' do
+ it { is_expected.to belong_to(:group) }
+ it { is_expected.to belong_to(:namespace) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:namespace) }
+ end
+
+ describe 'scopes' do
+ let_it_be(:group1) { create(:group) }
+ let_it_be(:group2) { create(:group) }
+ let_it_be(:cache_file1) { create(:composer_cache_file, file_sha256: '123456', group: group1) }
+ let_it_be(:cache_file2) { create(:composer_cache_file, delete_at: 2.days.from_now, file_sha256: '456778', group: group2) }
+
+ describe '.with_namespace' do
+ subject { described_class.with_namespace(group1) }
+
+ it { is_expected.to eq [cache_file1] }
+ end
+
+ describe '.with_sha' do
+ subject { described_class.with_sha('123456') }
+
+ it { is_expected.to eq [cache_file1] }
+ end
+ end
+end
diff --git a/spec/models/packages/composer/metadatum_spec.rb b/spec/models/packages/composer/metadatum_spec.rb
index ae53532696b..1c888f1563c 100644
--- a/spec/models/packages/composer/metadatum_spec.rb
+++ b/spec/models/packages/composer/metadatum_spec.rb
@@ -11,4 +11,20 @@ RSpec.describe Packages::Composer::Metadatum, type: :model do
it { is_expected.to validate_presence_of(:target_sha) }
it { is_expected.to validate_presence_of(:composer_json) }
end
+
+ describe 'scopes' do
+ let_it_be(:package_name) { 'sample-project' }
+ let_it_be(:json) { { 'name' => package_name } }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :custom_repo, files: { 'composer.json' => json.to_json }, group: group) }
+ let_it_be(:package1) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '1.0.0', json: json) }
+ let_it_be(:package2) { create(:composer_package, :with_metadatum, project: project, name: 'other-name', version: '1.0.0', json: json) }
+ let_it_be(:package3) { create(:pypi_package, name: package_name, project: project) }
+
+ describe '.for_package' do
+ subject { described_class.for_package(package_name, project.id) }
+
+ it { is_expected.to eq [package1.composer_metadatum] }
+ end
+ end
end
diff --git a/spec/models/packages/debian/group_component_file_spec.rb b/spec/models/packages/debian/group_component_file_spec.rb
new file mode 100644
index 00000000000..bf33ca138c3
--- /dev/null
+++ b/spec/models/packages/debian/group_component_file_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Debian::GroupComponentFile do
+ it_behaves_like 'Debian Component File', :group, false
+end
diff --git a/spec/models/packages/debian/group_component_spec.rb b/spec/models/packages/debian/group_component_spec.rb
new file mode 100644
index 00000000000..f288ebbe5df
--- /dev/null
+++ b/spec/models/packages/debian/group_component_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Debian::GroupComponent do
+ it_behaves_like 'Debian Distribution Component', :debian_group_component, :group, false
+end
diff --git a/spec/models/packages/debian/project_component_file_spec.rb b/spec/models/packages/debian/project_component_file_spec.rb
new file mode 100644
index 00000000000..5dfc47c14c0
--- /dev/null
+++ b/spec/models/packages/debian/project_component_file_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Debian::ProjectComponentFile do
+ it_behaves_like 'Debian Component File', :project, true
+end
diff --git a/spec/models/packages/debian/project_component_spec.rb b/spec/models/packages/debian/project_component_spec.rb
new file mode 100644
index 00000000000..4b041068b8d
--- /dev/null
+++ b/spec/models/packages/debian/project_component_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Debian::ProjectComponent do
+ it_behaves_like 'Debian Distribution Component', :debian_project_component, :project, true
+end
diff --git a/spec/models/packages/debian/publication_spec.rb b/spec/models/packages/debian/publication_spec.rb
new file mode 100644
index 00000000000..0ed056f499b
--- /dev/null
+++ b/spec/models/packages/debian/publication_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::Debian::Publication, type: :model do
+ let_it_be_with_reload(:publication) { create(:debian_publication) }
+
+ subject { publication }
+
+ describe 'relationships' do
+ it { is_expected.to belong_to(:package).inverse_of(:debian_publication).class_name('Packages::Package') }
+ it { is_expected.to belong_to(:distribution).inverse_of(:publications).class_name('Packages::Debian::ProjectDistribution').with_foreign_key(:distribution_id) }
+ end
+
+ describe 'validations' do
+ describe '#package' do
+ it { is_expected.to validate_presence_of(:package) }
+ end
+
+ describe '#valid_debian_package_type' do
+ context 'with package type not being Debian' do
+ before do
+ publication.package.package_type = 'generic'
+ end
+
+ it 'will not allow package type not being Debian' do
+ expect(publication).not_to be_valid
+ expect(publication.errors.to_a).to eq(['Package type must be Debian'])
+ end
+ end
+
+ context 'with package not being a Debian package' do
+ before do
+ publication.package.version = nil
+ end
+
+ it 'will not allow package not being a distribution' do
+ expect(publication).not_to be_valid
+ expect(publication.errors.to_a).to eq(['Package must be a Debian package'])
+ end
+ end
+ end
+
+ describe '#distribution' do
+ it { is_expected.to validate_presence_of(:distribution) }
+ end
+ end
+end
diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb
index 6645db33503..6c55d37b95f 100644
--- a/spec/models/packages/package_spec.rb
+++ b/spec/models/packages/package_spec.rb
@@ -4,6 +4,8 @@ require 'spec_helper'
RSpec.describe Packages::Package, type: :model do
include SortingHelper
+ it_behaves_like 'having unique enum values'
+
describe 'relationships' do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:creator) }
@@ -14,7 +16,10 @@ RSpec.describe Packages::Package, type: :model do
it { is_expected.to have_many(:pipelines).through(:build_infos) }
it { is_expected.to have_one(:conan_metadatum).inverse_of(:package) }
it { is_expected.to have_one(:maven_metadatum).inverse_of(:package) }
+ it { is_expected.to have_one(:debian_publication).inverse_of(:package).class_name('Packages::Debian::Publication') }
+ it { is_expected.to have_one(:debian_distribution).through(:debian_publication).source(:distribution).inverse_of(:packages).class_name('Packages::Debian::ProjectDistribution') }
it { is_expected.to have_one(:nuget_metadatum).inverse_of(:package) }
+ it { is_expected.to have_one(:rubygems_metadatum).inverse_of(:package) }
end
describe '.with_composer_target' do
@@ -374,7 +379,28 @@ RSpec.describe Packages::Package, type: :model do
end
end
- Packages::Package.package_types.keys.without('conan').each do |pt|
+ describe "#unique_debian_package_name" do
+ let!(:package) { create(:debian_package) }
+
+ it "will allow a Debian package with same project, name and version, but different distribution" do
+ new_package = build(:debian_package, project: package.project, name: package.name, version: package.version)
+ expect(new_package).to be_valid
+ end
+
+ it "will not allow a Debian package with same project, name, version and distribution" do
+ new_package = build(:debian_package, project: package.project, name: package.name, version: package.version)
+ new_package.debian_publication.distribution = package.debian_publication.distribution
+ expect(new_package).not_to be_valid
+ expect(new_package.errors.to_a).to include('Debian package already exists in Distribution')
+ end
+
+ it "will allow a Debian package with same project, name, version, but no distribution" do
+ new_package = build(:debian_package, project: package.project, name: package.name, version: package.version, published_in: nil)
+ expect(new_package).to be_valid
+ end
+ end
+
+ Packages::Package.package_types.keys.without('conan', 'debian').each do |pt|
context "project id, name, version and package type uniqueness for package type #{pt}" do
let(:package) { create("#{pt}_package") }
@@ -581,6 +607,28 @@ RSpec.describe Packages::Package, type: :model do
it { is_expected.to match_array([pypi_package]) }
end
+
+ describe '.displayable' do
+ let_it_be(:hidden_package) { create(:maven_package, :hidden) }
+ let_it_be(:processing_package) { create(:maven_package, :processing) }
+
+ subject { described_class.displayable }
+
+ it 'does not include hidden packages', :aggregate_failures do
+ is_expected.not_to include(hidden_package)
+ is_expected.not_to include(processing_package)
+ end
+ end
+
+ describe '.with_status' do
+ let_it_be(:hidden_package) { create(:maven_package, :hidden) }
+
+ subject { described_class.with_status(:hidden) }
+
+ it 'returns packages with specified status' do
+ is_expected.to match_array([hidden_package])
+ end
+ end
end
describe '.select_distinct_name' do
diff --git a/spec/models/packages/rubygems/metadatum_spec.rb b/spec/models/packages/rubygems/metadatum_spec.rb
new file mode 100644
index 00000000000..e99a07c7731
--- /dev/null
+++ b/spec/models/packages/rubygems/metadatum_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::Rubygems::Metadatum, type: :model do
+ describe 'relationships' do
+ it { is_expected.to belong_to(:package) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:package) }
+
+ describe '#rubygems_package_type' do
+ it 'will not allow a package with a different package_type' do
+ package = build('conan_package')
+ rubygems_metadatum = build('rubygems_metadatum', package: package)
+
+ expect(rubygems_metadatum).not_to be_valid
+ expect(rubygems_metadatum.errors.to_a).to include('Package type must be RubyGems')
+ end
+ end
+ end
+end
diff --git a/spec/models/pages/lookup_path_spec.rb b/spec/models/pages/lookup_path_spec.rb
index 30712af6b32..0a2b04f1a7c 100644
--- a/spec/models/pages/lookup_path_spec.rb
+++ b/spec/models/pages/lookup_path_spec.rb
@@ -56,6 +56,15 @@ RSpec.describe Pages::LookupPath do
include_examples 'uses disk storage'
+ it 'return nil when legacy storage is disabled and there is no deployment' do
+ stub_feature_flags(pages_serve_from_legacy_storage: false)
+ expect(Gitlab::ErrorTracking).to receive(:track_exception)
+ .with(described_class::LegacyStorageDisabledError)
+ .and_call_original
+
+ expect(source).to eq(nil)
+ end
+
context 'when there is pages deployment' do
let(:deployment) { create(:pages_deployment, project: project) }
@@ -115,6 +124,35 @@ RSpec.describe Pages::LookupPath do
include_examples 'uses disk storage'
end
+
+ context 'when deployment were created during migration' do
+ before do
+ allow(deployment).to receive(:migrated?).and_return(true)
+ end
+
+ it 'uses deployment from object storage' do
+ freeze_time do
+ expect(source).to(
+ eq({
+ type: 'zip',
+ path: deployment.file.url(expire_at: 1.day.from_now),
+ global_id: "gid://gitlab/PagesDeployment/#{deployment.id}",
+ sha256: deployment.file_sha256,
+ file_size: deployment.size,
+ file_count: deployment.file_count
+ })
+ )
+ end
+ end
+
+ context 'when pages_serve_from_migrated_zip feature flag is disabled' do
+ before do
+ stub_feature_flags(pages_serve_from_migrated_zip: false)
+ end
+
+ include_examples 'uses disk storage'
+ end
+ end
end
end
diff --git a/spec/models/pages/virtual_domain_spec.rb b/spec/models/pages/virtual_domain_spec.rb
index 38f5f4d2538..29c14cbeb3e 100644
--- a/spec/models/pages/virtual_domain_spec.rb
+++ b/spec/models/pages/virtual_domain_spec.rb
@@ -26,31 +26,34 @@ RSpec.describe Pages::VirtualDomain do
describe '#lookup_paths' do
let(:project_a) { instance_double(Project) }
- let(:project_z) { instance_double(Project) }
- let(:pages_lookup_path_a) { instance_double(Pages::LookupPath, prefix: 'aaa') }
- let(:pages_lookup_path_z) { instance_double(Pages::LookupPath, prefix: 'zzz') }
+ let(:project_b) { instance_double(Project) }
+ let(:project_c) { instance_double(Project) }
+ let(:pages_lookup_path_a) { instance_double(Pages::LookupPath, prefix: 'aaa', source: { type: 'zip', path: 'https://example.com' }) }
+ let(:pages_lookup_path_b) { instance_double(Pages::LookupPath, prefix: 'bbb', source: { type: 'zip', path: 'https://example.com' }) }
+ let(:pages_lookup_path_without_source) { instance_double(Pages::LookupPath, prefix: 'ccc', source: nil) }
context 'when there is pages domain provided' do
let(:domain) { instance_double(PagesDomain) }
- subject(:virtual_domain) { described_class.new([project_a, project_z], domain: domain) }
+ subject(:virtual_domain) { described_class.new([project_a, project_b, project_c], domain: domain) }
it 'returns collection of projects pages lookup paths sorted by prefix in reverse' do
expect(project_a).to receive(:pages_lookup_path).with(domain: domain, trim_prefix: nil).and_return(pages_lookup_path_a)
- expect(project_z).to receive(:pages_lookup_path).with(domain: domain, trim_prefix: nil).and_return(pages_lookup_path_z)
+ expect(project_b).to receive(:pages_lookup_path).with(domain: domain, trim_prefix: nil).and_return(pages_lookup_path_b)
+ expect(project_c).to receive(:pages_lookup_path).with(domain: domain, trim_prefix: nil).and_return(pages_lookup_path_without_source)
- expect(virtual_domain.lookup_paths).to eq([pages_lookup_path_z, pages_lookup_path_a])
+ expect(virtual_domain.lookup_paths).to eq([pages_lookup_path_b, pages_lookup_path_a])
end
end
context 'when there is trim_prefix provided' do
- subject(:virtual_domain) { described_class.new([project_a, project_z], trim_prefix: 'group/') }
+ subject(:virtual_domain) { described_class.new([project_a, project_b], trim_prefix: 'group/') }
it 'returns collection of projects pages lookup paths sorted by prefix in reverse' do
expect(project_a).to receive(:pages_lookup_path).with(trim_prefix: 'group/', domain: nil).and_return(pages_lookup_path_a)
- expect(project_z).to receive(:pages_lookup_path).with(trim_prefix: 'group/', domain: nil).and_return(pages_lookup_path_z)
+ expect(project_b).to receive(:pages_lookup_path).with(trim_prefix: 'group/', domain: nil).and_return(pages_lookup_path_b)
- expect(virtual_domain.lookup_paths).to eq([pages_lookup_path_z, pages_lookup_path_a])
+ expect(virtual_domain.lookup_paths).to eq([pages_lookup_path_b, pages_lookup_path_a])
end
end
end
diff --git a/spec/models/pages_deployment_spec.rb b/spec/models/pages_deployment_spec.rb
index e83cbc15004..029eb8e513a 100644
--- a/spec/models/pages_deployment_spec.rb
+++ b/spec/models/pages_deployment_spec.rb
@@ -26,6 +26,46 @@ RSpec.describe PagesDeployment do
end
end
+ describe '.migrated_from_legacy_storage' do
+ it 'only returns migrated deployments' do
+ project = create(:project)
+ migrated_deployment = create_migrated_deployment(project)
+ # create one other deployment
+ create(:pages_deployment, project: project)
+
+ expect(described_class.migrated_from_legacy_storage).to eq([migrated_deployment])
+ end
+ end
+
+ describe '#migrated?' do
+ it 'returns false for normal deployment' do
+ deployment = create(:pages_deployment)
+
+ expect(deployment.migrated?).to eq(false)
+ end
+
+ it 'returns true for migrated deployment' do
+ project = create(:project)
+ deployment = create_migrated_deployment(project)
+
+ expect(deployment.migrated?).to eq(true)
+ end
+ end
+
+ def create_migrated_deployment(project)
+ public_path = File.join(project.pages_path, "public")
+ FileUtils.mkdir_p(public_path)
+ File.open(File.join(project.pages_path, "public/index.html"), "w") do |f|
+ f.write("Hello!")
+ end
+
+ expect(::Pages::MigrateLegacyStorageToDeploymentService.new(project).execute[:status]).to eq(:success)
+
+ project.reload.pages_metadatum.pages_deployment
+ ensure
+ FileUtils.rm_rf(public_path)
+ end
+
describe 'default for file_store' do
let(:project) { create(:project) }
let(:deployment) do
diff --git a/spec/models/project_ci_cd_setting_spec.rb b/spec/models/project_ci_cd_setting_spec.rb
index 698465e854a..406485d8cc8 100644
--- a/spec/models/project_ci_cd_setting_spec.rb
+++ b/spec/models/project_ci_cd_setting_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe ProjectCiCdSetting do
+ using RSpec::Parameterized::TableSyntax
+
describe 'validations' do
it 'validates default_git_depth is between 0 and 1000 or nil' do
expect(subject).to validate_numericality_of(:default_git_depth)
@@ -36,4 +38,39 @@ RSpec.describe ProjectCiCdSetting do
expect(project.reload.ci_cd_settings.default_git_depth).to eq(0)
end
end
+
+ describe '#keep_latest_artifacts_available?' do
+ let(:attrs) { { keep_latest_artifact: project_enabled } }
+ let(:project_settings) { described_class.new(attrs) }
+
+ subject { project_settings.keep_latest_artifacts_available? }
+
+ context 'without application setting record' do
+ where(:project_enabled, :result_keep_latest_artifact) do
+ false | false
+ true | true
+ end
+
+ with_them do
+ it { expect(subject).to eq(result_keep_latest_artifact) }
+ end
+ end
+
+ context 'with application setting record' do
+ where(:instance_enabled, :project_enabled, :result_keep_latest_artifact) do
+ false | false | false
+ false | true | false
+ true | false | false
+ true | true | true
+ end
+
+ before do
+ Gitlab::CurrentSettings.current_application_settings.update!(keep_latest_artifact: instance_enabled)
+ end
+
+ with_them do
+ it { expect(subject).to eq(result_keep_latest_artifact) }
+ end
+ end
+ end
end
diff --git a/spec/models/project_services/alerts_service_spec.rb b/spec/models/project_services/alerts_service_spec.rb
deleted file mode 100644
index 75b91c29914..00000000000
--- a/spec/models/project_services/alerts_service_spec.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-# AlertsService is stripped down to only required methods
-# to avoid errors loading integration-related pages if
-# records are present.
-RSpec.describe AlertsService do
- let_it_be(:project) { create(:project) }
- subject(:service) { described_class.new(project: project) }
-
- it { is_expected.to be_valid }
-
- describe '#to_param' do
- subject { service.to_param }
-
- it { is_expected.to eq('alerts') }
- end
-
- describe '#supported_events' do
- subject { service.supported_events }
-
- it { is_expected.to be_empty }
- end
-
- describe '#save' do
- it 'prevents records from being created or updated' do
- expect(Gitlab::ProjectServiceLogger).to receive(:error).with(
- hash_including(message: 'Prevented attempt to save or update deprecated AlertsService')
- )
-
- expect(service.save).to be_falsey
-
- expect(service.errors.full_messages).to include(
- 'Alerts endpoint is deprecated and should not be created or modified. Use HTTP Integrations instead.'
- )
- end
- end
-end
diff --git a/spec/models/project_services/chat_notification_service_spec.rb b/spec/models/project_services/chat_notification_service_spec.rb
index 77a1377c138..476d99364b6 100644
--- a/spec/models/project_services/chat_notification_service_spec.rb
+++ b/spec/models/project_services/chat_notification_service_spec.rb
@@ -75,6 +75,39 @@ RSpec.describe ChatNotificationService do
end
end
+ context 'when the data object has a label' do
+ let(:label) { create(:label, project: project, name: 'Bug')}
+ let(:issue) { create(:labeled_issue, project: project, labels: [label]) }
+ let(:note) { create(:note, noteable: issue, project: project)}
+ let(:data) { Gitlab::DataBuilder::Note.build(note, user) }
+
+ it 'notifies the chat service' do
+ expect(chat_service).to receive(:notify).with(any_args)
+
+ chat_service.execute(data)
+ end
+
+ context 'and the chat_service has a label filter that does not matches the label' do
+ subject(:chat_service) { described_class.new(labels_to_be_notified: '~some random label') }
+
+ it 'does not notify the chat service' do
+ expect(chat_service).not_to receive(:notify)
+
+ chat_service.execute(data)
+ end
+ end
+
+ context 'and the chat_service has a label filter that matches the label' do
+ subject(:chat_service) { described_class.new(labels_to_be_notified: '~Backend, ~Bug') }
+
+ it 'notifies the chat service' do
+ expect(chat_service).to receive(:notify).with(any_args)
+
+ chat_service.execute(data)
+ end
+ end
+ end
+
context 'with "channel" property' do
before do
allow(chat_service).to receive(:channel).and_return(channel)
diff --git a/spec/models/project_services/confluence_service_spec.rb b/spec/models/project_services/confluence_service_spec.rb
index 5d153b17070..6c7ba2c9f32 100644
--- a/spec/models/project_services/confluence_service_spec.rb
+++ b/spec/models/project_services/confluence_service_spec.rb
@@ -43,13 +43,13 @@ RSpec.describe ConfluenceService do
end
end
- describe '#detailed_description' 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
- expect(subject.detailed_description).to include(Gitlab::Routing.url_helpers.project_wikis_url(project))
+ expect(subject.help).to include(Gitlab::Routing.url_helpers.project_wikis_url(project))
end
context 'when the project wiki is not enabled' do
@@ -60,7 +60,7 @@ RSpec.describe ConfluenceService do
[true, false].each do |active|
subject.active = active
- expect(subject.detailed_description).to be_nil
+ expect(subject.help).to be_nil
end
end
end
diff --git a/spec/models/project_services/datadog_service_spec.rb b/spec/models/project_services/datadog_service_spec.rb
index 1d9f49e4824..d15ea1f351b 100644
--- a/spec/models/project_services/datadog_service_spec.rb
+++ b/spec/models/project_services/datadog_service_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe DatadogService, :model do
let(:active) { true }
let(:dd_site) { 'datadoghq.com' }
let(:default_url) { 'https://webhooks-http-intake.logs.datadoghq.com/v1/input/' }
- let(:api_url) { nil }
+ let(:api_url) { '' }
let(:api_key) { SecureRandom.hex(32) }
let(:dd_env) { 'ci' }
let(:dd_service) { 'awesome-gitlab' }
@@ -22,13 +22,11 @@ RSpec.describe DatadogService, :model do
described_class.new(
active: active,
project: project,
- properties: {
- datadog_site: dd_site,
- api_url: api_url,
- api_key: api_key,
- datadog_env: dd_env,
- datadog_service: dd_service
- }
+ datadog_site: dd_site,
+ api_url: api_url,
+ api_key: api_key,
+ datadog_env: dd_env,
+ datadog_service: dd_service
)
end
@@ -58,7 +56,7 @@ RSpec.describe DatadogService, :model do
context 'when selecting site' do
let(:dd_site) { 'datadoghq.com' }
- let(:api_url) { nil }
+ let(:api_url) { '' }
it { is_expected.to validate_presence_of(:datadog_site) }
it { is_expected.not_to validate_presence_of(:api_url) }
@@ -66,7 +64,7 @@ RSpec.describe DatadogService, :model do
end
context 'with custom api_url' do
- let(:dd_site) { nil }
+ let(:dd_site) { '' }
let(:api_url) { 'https://webhooks-http-intake.logs.datad0g.com/v1/input/' }
it { is_expected.not_to validate_presence_of(:datadog_site) }
@@ -76,13 +74,21 @@ RSpec.describe DatadogService, :model do
end
context 'when missing site and api_url' do
- let(:dd_site) { nil }
- let(:api_url) { nil }
+ let(:dd_site) { '' }
+ let(:api_url) { '' }
it { is_expected.not_to be_valid }
it { is_expected.to validate_presence_of(:datadog_site) }
it { is_expected.to validate_presence_of(:api_url) }
end
+
+ context 'when providing both site and api_url' do
+ let(:dd_site) { 'datadoghq.com' }
+ let(:api_url) { default_url }
+
+ it { is_expected.not_to allow_value('datadog hq.com').for(:datadog_site) }
+ it { is_expected.not_to allow_value('example.com').for(:api_url) }
+ end
end
context 'when service is not active' do
@@ -113,8 +119,8 @@ RSpec.describe DatadogService, :model do
end
context 'without optional params' do
- let(:dd_service) { nil }
- let(:dd_env) { nil }
+ let(:dd_service) { '' }
+ let(:dd_env) { '' }
it { is_expected.to eq(default_url + api_key) }
end
@@ -126,7 +132,7 @@ RSpec.describe DatadogService, :model do
it { is_expected.to eq("https://app.#{dd_site}/account/settings#api") }
context 'with unset datadog_site' do
- let(:dd_site) { nil }
+ let(:dd_site) { '' }
it { is_expected.to eq("https://docs.datadoghq.com/account_management/api-app-keys/") }
end
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index cd0873bddd2..78bd0e91208 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -6,6 +6,8 @@ RSpec.describe JiraService do
include AssetsHelpers
let_it_be(:project) { create(:project, :repository) }
+
+ let(:current_user) { build_stubbed(:user) }
let(:url) { 'http://jira.example.com' }
let(:api_url) { 'http://api-jira.example.com' }
let(:username) { 'jira-username' }
@@ -456,6 +458,16 @@ RSpec.describe JiraService do
expect(WebMock).to have_requested(:get, issue_url)
end
+
+ context 'with options' do
+ let(:issue_url) { "#{url}/rest/api/2/issue/#{issue_key}?expand=renderedFields" }
+
+ it 'calls the Jira API with the options to get the issue' do
+ jira_service.find_issue(issue_key, rendered_fields: true)
+
+ expect(WebMock).to have_requested(:get, issue_url)
+ end
+ end
end
describe '#close_issue' do
@@ -498,25 +510,38 @@ RSpec.describe JiraService do
WebMock.stub_request(:post, @remote_link_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password))
end
+ let(:external_issue) { ExternalIssue.new('JIRA-123', project) }
+
+ def close_issue
+ @jira_service.close_issue(resource, external_issue, current_user)
+ end
+
it 'calls Jira API' do
- @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project))
+ close_issue
expect(WebMock).to have_requested(:post, @comment_url).with(
body: /Issue solved with/
).once
end
+ it 'tracks usage' do
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter)
+ .to receive(:track_event)
+ .with('i_ecosystem_jira_service_close_issue', values: current_user.id)
+
+ close_issue
+ end
+
it 'does not fail if remote_link.all on issue returns nil' do
allow(JIRA::Resource::Remotelink).to receive(:all).and_return(nil)
- expect { @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project)) }
- .not_to raise_error
+ expect { close_issue }.not_to raise_error
end
# Check https://developer.atlassian.com/jiradev/jira-platform/guides/other/guide-jira-remote-issue-links/fields-in-remote-issue-links
# for more information
it 'creates Remote Link reference in Jira for comment' do
- @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project))
+ close_issue
favicon_path = "http://localhost/assets/#{find_asset('favicon.png').digest_path}"
@@ -540,7 +565,7 @@ RSpec.describe JiraService do
context 'when "comment_on_event_enabled" is set to false' do
it 'creates Remote Link reference but does not create comment' do
allow(@jira_service).to receive_messages(comment_on_event_enabled: false)
- @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project))
+ close_issue
expect(WebMock).not_to have_requested(:post, @comment_url)
expect(WebMock).to have_requested(:post, @remote_link_url)
@@ -562,7 +587,7 @@ RSpec.describe JiraService do
expect(remote_link).to receive(:save!)
- @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project))
+ close_issue
expect(WebMock).not_to have_requested(:post, @comment_url)
end
@@ -571,7 +596,7 @@ RSpec.describe JiraService do
it 'does not send comment or remote links to issues already closed' do
allow_any_instance_of(JIRA::Resource::Issue).to receive(:resolution).and_return(true)
- @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project))
+ close_issue
expect(WebMock).not_to have_requested(:post, @comment_url)
expect(WebMock).not_to have_requested(:post, @remote_link_url)
@@ -580,7 +605,7 @@ RSpec.describe JiraService do
it 'does not send comment or remote links to issues with unknown resolution' do
allow_any_instance_of(JIRA::Resource::Issue).to receive(:respond_to?).with(:resolution).and_return(false)
- @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project))
+ close_issue
expect(WebMock).not_to have_requested(:post, @comment_url)
expect(WebMock).not_to have_requested(:post, @remote_link_url)
@@ -589,7 +614,7 @@ RSpec.describe JiraService do
it 'references the GitLab commit' do
stub_config_setting(base_url: custom_base_url)
- @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project))
+ close_issue
expect(WebMock).to have_requested(:post, @comment_url).with(
body: %r{#{custom_base_url}/#{project.full_path}/-/commit/#{commit_id}}
@@ -604,7 +629,7 @@ RSpec.describe JiraService do
{ script_name: '/gitlab' }
end
- @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project))
+ close_issue
expect(WebMock).to have_requested(:post, @comment_url).with(
body: %r{#{Gitlab.config.gitlab.url}/#{project.full_path}/-/commit/#{commit_id}}
@@ -615,7 +640,7 @@ RSpec.describe JiraService do
allow(@jira_service).to receive(:log_error)
WebMock.stub_request(:post, @transitions_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)).and_raise("Bad Request")
- @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project))
+ close_issue
expect(@jira_service).to have_received(:log_error).with(
"Issue transition failed",
@@ -628,7 +653,7 @@ RSpec.describe JiraService do
end
it 'calls the api with jira_issue_transition_id' do
- @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project))
+ close_issue
expect(WebMock).to have_requested(:post, @transitions_url).with(
body: /999/
@@ -639,7 +664,7 @@ RSpec.describe JiraService do
it 'calls the api with transition ids separated by comma' do
allow(@jira_service).to receive_messages(jira_issue_transition_id: '1,2,3')
- @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project))
+ close_issue
1.upto(3) do |transition_id|
expect(WebMock).to have_requested(:post, @transitions_url).with(
@@ -651,7 +676,7 @@ RSpec.describe JiraService do
it 'calls the api with transition ids separated by semicolon' do
allow(@jira_service).to receive_messages(jira_issue_transition_id: '1;2;3')
- @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project))
+ close_issue
1.upto(3) do |transition_id|
expect(WebMock).to have_requested(:post, @transitions_url).with(
@@ -702,6 +727,14 @@ RSpec.describe JiraService do
body: /mentioned this issue in/
).once
end
+
+ it 'tracks usage' do
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter)
+ .to receive(:track_event)
+ .with('i_ecosystem_jira_service_cross_reference', values: user.id)
+
+ subject
+ end
end
context 'when resource is a commit' do
diff --git a/spec/models/project_services/jira_tracker_data_spec.rb b/spec/models/project_services/jira_tracker_data_spec.rb
index f2e2fa65e93..46194efcb3d 100644
--- a/spec/models/project_services/jira_tracker_data_spec.rb
+++ b/spec/models/project_services/jira_tracker_data_spec.rb
@@ -3,13 +3,28 @@
require 'spec_helper'
RSpec.describe JiraTrackerData do
- let(:service) { build(:jira_service) }
-
- describe 'Associations' do
+ describe 'associations' do
it { is_expected.to belong_to(:service) }
end
describe 'deployment_type' do
it { is_expected.to define_enum_for(:deployment_type).with_values([:unknown, :server, :cloud]).with_prefix(:deployment) }
end
+
+ describe 'proxy settings' do
+ it { is_expected.to validate_length_of(:proxy_address).is_at_most(2048) }
+ it { is_expected.to validate_length_of(:proxy_port).is_at_most(5) }
+ it { is_expected.to validate_length_of(:proxy_username).is_at_most(255) }
+ it { is_expected.to validate_length_of(:proxy_password).is_at_most(255) }
+ end
+
+ describe 'encrypted attributes' do
+ subject { described_class.encrypted_attributes.keys }
+
+ it {
+ is_expected.to contain_exactly(
+ :api_url, :password, :proxy_address, :proxy_password, :proxy_port, :proxy_username, :url, :username
+ )
+ }
+ end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index a2b51684d4d..fd7975bf65d 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -21,6 +21,7 @@ RSpec.describe Project, factory_default: :keep do
it { is_expected.to have_many(:services) }
it { is_expected.to have_many(:events) }
it { is_expected.to have_many(:merge_requests) }
+ it { is_expected.to have_many(:merge_request_metrics).class_name('MergeRequest::Metrics') }
it { is_expected.to have_many(:issues) }
it { is_expected.to have_many(:milestones) }
it { is_expected.to have_many(:iterations) }
@@ -558,6 +559,25 @@ RSpec.describe Project, factory_default: :keep do
end
end
+ describe '#default_pipeline_lock' do
+ let(:project) { build_stubbed(:project) }
+
+ subject { project.default_pipeline_lock }
+
+ where(:keep_latest_artifact_enabled, :result_pipeline_locked) do
+ false | :unlocked
+ true | :artifacts_locked
+ end
+
+ before do
+ allow(project).to receive(:keep_latest_artifacts_available?).and_return(keep_latest_artifact_enabled)
+ end
+
+ with_them do
+ it { expect(subject).to eq(result_pipeline_locked) }
+ end
+ end
+
describe '#autoclose_referenced_issues' do
context 'when DB entry is nil' do
let(:project) { build(:project, autoclose_referenced_issues: nil) }
@@ -1002,103 +1022,125 @@ RSpec.describe Project, factory_default: :keep do
end
end
- describe '#external_issue_tracker' do
- let(:project) { create(:project) }
- let(:ext_project) { create(:redmine_project) }
+ describe '#has_wiki?' do
+ let(:no_wiki_project) { create(:project, :wiki_disabled, has_external_wiki: false) }
+ let(:wiki_enabled_project) { create(:project) }
+ let(:external_wiki_project) { create(:project, has_external_wiki: true) }
- context 'on existing projects with no value for has_external_issue_tracker' do
- before do
- project.update_column(:has_external_issue_tracker, nil)
- ext_project.update_column(:has_external_issue_tracker, nil)
+ it 'returns true if project is wiki enabled or has external wiki' do
+ expect(wiki_enabled_project).to have_wiki
+ expect(external_wiki_project).to have_wiki
+ expect(no_wiki_project).not_to have_wiki
+ end
+ end
+
+ describe '#default_owner' do
+ let_it_be(:owner) { create(:user) }
+ let_it_be(:namespace) { create(:namespace, owner: owner) }
+
+ context 'the project does not have a group' do
+ let(:project) { build(:project, namespace: namespace) }
+
+ it 'is the namespace owner' do
+ expect(project.default_owner).to eq(owner)
end
+ end
- it 'updates the has_external_issue_tracker boolean' do
- expect do
- project.external_issue_tracker
- end.to change { project.reload.has_external_issue_tracker }.to(false)
+ context 'the project is in a group' do
+ let(:group) { build(:group) }
+ let(:project) { build(:project, group: group, namespace: namespace) }
- expect do
- ext_project.external_issue_tracker
- end.to change { ext_project.reload.has_external_issue_tracker }.to(true)
+ it 'is the group owner' do
+ allow(group).to receive(:default_owner).and_return(Object.new)
+
+ expect(project.default_owner).to eq(group.default_owner)
end
end
+ end
+
+ describe '#external_issue_tracker' do
+ it 'sets Project#has_external_issue_tracker when it is nil' do
+ project_with_no_tracker = create(:project, has_external_issue_tracker: nil)
+ project_with_tracker = create(:redmine_project, has_external_issue_tracker: nil)
+
+ expect do
+ project_with_no_tracker.external_issue_tracker
+ end.to change { project_with_no_tracker.reload.has_external_issue_tracker }.from(nil).to(false)
+
+ expect do
+ project_with_tracker.external_issue_tracker
+ end.to change { project_with_tracker.reload.has_external_issue_tracker }.from(nil).to(true)
+ end
it 'returns nil and does not query services when there is no external issue tracker' do
- expect(project).not_to receive(:services)
+ project = create(:project)
+ expect(project).not_to receive(:services)
expect(project.external_issue_tracker).to eq(nil)
end
it 'retrieves external_issue_tracker querying services and cache it when there is external issue tracker' do
- ext_project.reload # Factory returns a project with changed attributes
- expect(ext_project).to receive(:services).once.and_call_original
+ project = create(:redmine_project)
- 2.times { expect(ext_project.external_issue_tracker).to be_a_kind_of(RedmineService) }
+ expect(project).to receive(:services).once.and_call_original
+ 2.times { expect(project.external_issue_tracker).to be_a_kind_of(RedmineService) }
end
end
- describe '#cache_has_external_issue_tracker' do
- let_it_be(:project) { create(:project, has_external_issue_tracker: nil) }
-
- it 'stores true if there is any external_issue_tracker' do
- services = double(:service, external_issue_trackers: [RedmineService.new])
- expect(project).to receive(:services).and_return(services)
+ describe '#has_external_issue_tracker' do
+ let_it_be(:project) { create(:project) }
- expect do
- project.cache_has_external_issue_tracker
- end.to change { project.has_external_issue_tracker}.to(true)
+ def subject
+ project.reload.has_external_issue_tracker
end
- it 'stores false if there is no external_issue_tracker' do
- services = double(:service, external_issue_trackers: [])
- expect(project).to receive(:services).and_return(services)
+ it 'is false when external issue tracker service is not active' do
+ create(:service, project: project, category: 'issue_tracker', active: false)
- expect do
- project.cache_has_external_issue_tracker
- end.to change { project.has_external_issue_tracker}.to(false)
+ is_expected.to eq(false)
end
- it 'does not cache data when in a read-only GitLab instance' do
- allow(Gitlab::Database).to receive(:read_only?) { true }
+ it 'is false when other service is active' do
+ create(:service, project: project, category: 'not_issue_tracker', active: true)
- expect do
- project.cache_has_external_issue_tracker
- end.not_to change { project.has_external_issue_tracker }
+ is_expected.to eq(false)
end
- end
- describe '#has_wiki?' do
- let(:no_wiki_project) { create(:project, :wiki_disabled, has_external_wiki: false) }
- let(:wiki_enabled_project) { create(:project) }
- let(:external_wiki_project) { create(:project, has_external_wiki: true) }
-
- it 'returns true if project is wiki enabled or has external wiki' do
- expect(wiki_enabled_project).to have_wiki
- expect(external_wiki_project).to have_wiki
- expect(no_wiki_project).not_to have_wiki
- end
- end
+ context 'when there is an active external issue tracker service' do
+ let!(:service) do
+ create(:service, project: project, type: 'JiraService', category: 'issue_tracker', active: true)
+ end
- describe '#default_owner' do
- let_it_be(:owner) { create(:user) }
- let_it_be(:namespace) { create(:namespace, owner: owner) }
+ specify { is_expected.to eq(true) }
- context 'the project does not have a group' do
- let(:project) { build(:project, namespace: namespace) }
+ it 'becomes false when external issue tracker service is destroyed' do
+ expect do
+ Service.find(service.id).delete
+ end.to change { subject }.to(false)
+ end
- it 'is the namespace owner' do
- expect(project.default_owner).to eq(owner)
+ it 'becomes false when external issue tracker service becomes inactive' do
+ expect do
+ service.update_column(:active, false)
+ end.to change { subject }.to(false)
end
- end
- context 'the project is in a group' do
- let(:group) { build(:group) }
- let(:project) { build(:project, group: group, namespace: namespace) }
+ context 'when there are two active external issue tracker services' do
+ let_it_be(:second_service) do
+ create(:service, project: project, type: 'CustomIssueTracker', category: 'issue_tracker', active: true)
+ end
- it 'is the group owner' do
- allow(group).to receive(:default_owner).and_return(Object.new)
+ it 'does not become false when external issue tracker service is destroyed' do
+ expect do
+ Service.find(service.id).delete
+ end.not_to change { subject }
+ end
- expect(project.default_owner).to eq(group.default_owner)
+ it 'does not become false when external issue tracker service becomes inactive' do
+ expect do
+ service.update_column(:active, false)
+ end.not_to change { subject }
+ end
end
end
end
@@ -1234,7 +1276,7 @@ RSpec.describe Project, factory_default: :keep do
it 'is false if avatar is html page' do
project.update_attribute(:avatar, 'uploads/avatar.html')
- expect(project.avatar_type).to eq(['file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico'])
+ expect(project.avatar_type).to eq(['file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico, webp'])
end
end
@@ -1529,10 +1571,7 @@ RSpec.describe Project, factory_default: :keep do
let(:project) { build(:project) }
it 'picks storage from ApplicationSetting' do
- expect_next_instance_of(ApplicationSetting) do |instance|
- expect(instance).to receive(:pick_repository_storage).and_return('picked')
- end
- expect(described_class).to receive(:pick_repository_storage).and_call_original
+ expect(Repository).to receive(:pick_storage_shard).and_return('picked')
expect(project.repository_storage).to eq('picked')
end
@@ -2227,8 +2266,6 @@ RSpec.describe Project, factory_default: :keep do
end
describe '#ci_config_path=' do
- using RSpec::Parameterized::TableSyntax
-
let(:project) { build_stubbed(:project) }
where(:default_ci_config_path, :project_ci_config_path, :expected_ci_config_path) do
@@ -2980,6 +3017,7 @@ RSpec.describe Project, factory_default: :keep do
it_behaves_like 'can housekeep repository' do
let(:resource) { build_stubbed(:project) }
let(:resource_key) { 'projects' }
+ let(:expected_worker_class) { Projects::GitGarbageCollectWorker }
end
describe '#deployment_variables' do
@@ -3926,7 +3964,6 @@ RSpec.describe Project, factory_default: :keep do
describe '.filter_by_feature_visibility' do
include_context 'ProjectPolicyTable context'
include ProjectHelpers
- using RSpec::Parameterized::TableSyntax
let_it_be(:group) { create(:group) }
let!(:project) { create(:project, project_level, namespace: group ) }
@@ -4099,7 +4136,7 @@ RSpec.describe Project, factory_default: :keep do
end
end
- describe '#remove_pages' do
+ describe '#legacy_remove_pages' do
let(:project) { create(:project).tap { |project| project.mark_pages_as_deployed } }
let(:pages_metadatum) { project.pages_metadatum }
let(:namespace) { project.namespace }
@@ -4118,34 +4155,22 @@ RSpec.describe Project, factory_default: :keep do
expect_any_instance_of(Gitlab::PagesTransfer).to receive(:rename_project).and_return(true)
expect(PagesWorker).to receive(:perform_in).with(5.minutes, :remove, namespace.full_path, anything)
- expect { project.remove_pages }.to change { pages_metadatum.reload.deployed }.from(true).to(false)
+ expect { project.legacy_remove_pages }.to change { pages_metadatum.reload.deployed }.from(true).to(false)
end
- it 'is run when the project is destroyed' do
- expect(project).to receive(:remove_pages).and_call_original
-
- expect { project.destroy }.not_to raise_error
- end
+ it 'does nothing if updates on legacy storage are disabled' do
+ stub_feature_flags(pages_update_legacy_storage: false)
- context 'when there is an old pages deployment' do
- let!(:old_deployment_from_another_project) { create(:pages_deployment) }
- let!(:old_deployment) { create(:pages_deployment, project: project) }
+ expect(Gitlab::PagesTransfer).not_to receive(:new)
+ expect(PagesWorker).not_to receive(:perform_in)
- it 'schedules a destruction of pages deployments' do
- expect(DestroyPagesDeploymentsWorker).to(
- receive(:perform_async).with(project.id)
- )
-
- project.remove_pages
- end
+ project.legacy_remove_pages
+ end
- it 'removes pages deployments', :sidekiq_inline do
- expect do
- project.remove_pages
- end.to change { PagesDeployment.count }.by(-1)
+ it 'is run when the project is destroyed' do
+ expect(project).to receive(:legacy_remove_pages).and_call_original
- expect(PagesDeployment.find_by_id(old_deployment.id)).to be_nil
- end
+ expect { project.destroy }.not_to raise_error
end
end
@@ -4176,8 +4201,6 @@ RSpec.describe Project, factory_default: :keep do
end
describe '#git_transfer_in_progress?' do
- using RSpec::Parameterized::TableSyntax
-
let(:project) { build(:project) }
subject { project.git_transfer_in_progress? }
@@ -5068,10 +5091,8 @@ RSpec.describe Project, factory_default: :keep do
it 'executes services with the specified scope' do
data = 'any data'
- expect(SlackService).to receive(:allocate).and_wrap_original do |method|
- method.call.tap do |instance|
- expect(instance).to receive(:async_execute).with(data).once
- end
+ expect_next_found_instance_of(SlackService) do |instance|
+ expect(instance).to receive(:async_execute).with(data).once
end
service.project.execute_services(data, :push_hooks)
@@ -5801,8 +5822,6 @@ RSpec.describe Project, factory_default: :keep do
end
describe 'validation #changing_shared_runners_enabled_is_allowed' do
- using RSpec::Parameterized::TableSyntax
-
where(:shared_runners_setting, :project_shared_runners_enabled, :valid_record) do
'enabled' | true | true
'enabled' | false | true
@@ -6025,8 +6044,6 @@ RSpec.describe Project, factory_default: :keep do
end
describe '#closest_setting' do
- using RSpec::Parameterized::TableSyntax
-
shared_examples_for 'fetching closest setting' do
let!(:namespace) { create(:namespace) }
let!(:project) { create(:project, namespace: namespace) }
@@ -6378,20 +6395,7 @@ RSpec.describe Project, factory_default: :keep do
describe 'with Debian Distributions' do
subject { create(:project) }
- let!(:distributions) { create_list(:debian_project_distribution, 2, :with_file, container: subject) }
-
- it 'removes distribution files on removal' do
- distribution_file_paths = distributions.map do |distribution|
- distribution.file.path
- end
-
- expect { subject.destroy }
- .to change {
- distribution_file_paths.select do |path|
- File.exist? path
- end.length
- }.from(distribution_file_paths.length).to(0)
- end
+ it_behaves_like 'model with Debian distributions'
end
describe '#environments_for_scope' do
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index 8001d009901..c04fc70deca 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -47,5 +47,6 @@ RSpec.describe ProjectWiki do
let_it_be(:resource) { create(:project_wiki) }
let(:resource_key) { 'project_wikis' }
+ let(:expected_worker_class) { Wikis::GitGarbageCollectWorker }
end
end
diff --git a/spec/models/prometheus_metric_spec.rb b/spec/models/prometheus_metric_spec.rb
index 9588167bbcc..a20f4edcf4a 100644
--- a/spec/models/prometheus_metric_spec.rb
+++ b/spec/models/prometheus_metric_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe PrometheusMetric do
+ using RSpec::Parameterized::TableSyntax
+
subject { build(:prometheus_metric) }
it_behaves_like 'having unique enum values'
@@ -14,8 +16,6 @@ RSpec.describe PrometheusMetric do
it { is_expected.to validate_uniqueness_of(:identifier).scoped_to(:project_id).allow_nil }
describe 'common metrics' do
- using RSpec::Parameterized::TableSyntax
-
where(:common, :with_project, :result) do
false | true | true
false | false | false
@@ -34,8 +34,6 @@ RSpec.describe PrometheusMetric do
end
describe '#query_series' do
- using RSpec::Parameterized::TableSyntax
-
where(:legend, :type) do
'Some other legend' | NilClass
'Status Code' | Array
@@ -72,8 +70,6 @@ RSpec.describe PrometheusMetric do
end
describe '#priority' do
- using RSpec::Parameterized::TableSyntax
-
where(:group, :priority) do
:nginx_ingress_vts | 10
:nginx_ingress | 10
@@ -97,8 +93,6 @@ RSpec.describe PrometheusMetric do
end
describe '#required_metrics' do
- using RSpec::Parameterized::TableSyntax
-
where(:group, :required_metrics) do
:nginx_ingress_vts | %w(nginx_upstream_responses_total nginx_upstream_response_msecs_avg)
:nginx_ingress | %w(nginx_ingress_controller_requests nginx_ingress_controller_ingress_upstream_latency_seconds_sum)
diff --git a/spec/models/protected_branch/push_access_level_spec.rb b/spec/models/protected_branch/push_access_level_spec.rb
index 051cb78a6b6..17a589f0485 100644
--- a/spec/models/protected_branch/push_access_level_spec.rb
+++ b/spec/models/protected_branch/push_access_level_spec.rb
@@ -54,16 +54,6 @@ RSpec.describe ProtectedBranch::PushAccessLevel do
specify do
expect(push_access_level.check_access(user)).to be_truthy
end
-
- context 'when the deploy_keys_on_protected_branches FF is false' do
- before do
- stub_feature_flags(deploy_keys_on_protected_branches: false)
- end
-
- it 'is false' do
- expect(push_access_level.check_access(user)).to be_falsey
- end
- end
end
context 'when the deploy key is not among the active keys of this project' do
diff --git a/spec/models/readme_blob_spec.rb b/spec/models/readme_blob_spec.rb
deleted file mode 100644
index 95622d55254..00000000000
--- a/spec/models/readme_blob_spec.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ReadmeBlob do
- include FakeBlobHelpers
-
- describe 'policy' do
- let(:project) { build(:project, :repository) }
-
- subject { described_class.new(fake_blob(path: 'README.md'), project.repository) }
-
- it 'works with policy' do
- expect(Ability.allowed?(project.creator, :read_blob, subject)).to be_truthy
- end
- end
-end
diff --git a/spec/models/release_highlight_spec.rb b/spec/models/release_highlight_spec.rb
index 749b9b8e1ab..60087278671 100644
--- a/spec/models/release_highlight_spec.rb
+++ b/spec/models/release_highlight_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ReleaseHighlight do
+RSpec.describe ReleaseHighlight, :clean_gitlab_redis_cache do
let(:fixture_dir_glob) { Dir.glob(File.join('spec', 'fixtures', 'whats_new', '*.yml')).grep(/\d*\_(\d*\_\d*)\.yml$/) }
before do
diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb
index b436c2e1088..209ac471210 100644
--- a/spec/models/release_spec.rb
+++ b/spec/models/release_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Release do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public, :repository) }
- let_it_be(:release) { create(:release, project: project, author: user) }
+ let(:release) { create(:release, project: project, author: user) }
it { expect(release).to be_valid }
@@ -89,6 +89,61 @@ RSpec.describe Release do
end
end
+ describe '#update' do
+ subject { release.update(params) }
+
+ context 'when links do not exist' do
+ context 'when params are specified for creation' do
+ let(:params) do
+ { links_attributes: [{ name: 'test', url: 'https://www.google.com/' }] }
+ end
+
+ it 'creates a link successfuly' do
+ is_expected.to eq(true)
+
+ expect(release.links.count).to eq(1)
+ expect(release.links.first.name).to eq('test')
+ expect(release.links.first.url).to eq('https://www.google.com/')
+ end
+ end
+ end
+
+ context 'when a link exists' do
+ let!(:link1) { create(:release_link, release: release, name: 'test1', url: 'https://www.google1.com/') }
+ let!(:link2) { create(:release_link, release: release, name: 'test2', url: 'https://www.google2.com/') }
+
+ before do
+ release.reload
+ end
+
+ context 'when params are specified for update' do
+ let(:params) do
+ { links_attributes: [{ id: link1.id, name: 'new' }] }
+ end
+
+ it 'updates the link successfully' do
+ is_expected.to eq(true)
+
+ expect(release.links.count).to eq(2)
+ expect(release.links.first.name).to eq('new')
+ end
+ end
+
+ context 'when params are specified for deletion' do
+ let(:params) do
+ { links_attributes: [{ id: link1.id, _destroy: true }] }
+ end
+
+ it 'removes the link successfuly' do
+ is_expected.to eq(true)
+
+ expect(release.links.count).to eq(1)
+ expect(release.links.first.name).to eq(link2.name)
+ end
+ end
+ end
+ end
+
describe '#sources' do
subject { release.sources }
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index dd54a701282..3a4de7ba279 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -483,12 +483,6 @@ RSpec.describe Repository do
it { is_expected.to be_an_instance_of(::Blob) }
end
- context 'readme blob on HEAD' do
- subject { repository.blob_at(repository.head_commit.sha, 'README.md') }
-
- it { is_expected.to be_an_instance_of(::ReadmeBlob) }
- end
-
context 'readme blob not on HEAD' do
subject { repository.blob_at(repository.find_branch('feature').target, 'README.md') }
@@ -1142,11 +1136,11 @@ RSpec.describe Repository do
expect(repository.license_key).to be_nil
end
- it 'returns nil when the content is not recognizable' do
+ it 'returns other when the content is not recognizable' do
repository.create_file(user, 'LICENSE', 'Gitlab B.V.',
message: 'Add LICENSE', branch_name: 'master')
- expect(repository.license_key).to be_nil
+ expect(repository.license_key).to eq('other')
end
it 'returns nil when the commit SHA does not exist' do
@@ -1186,11 +1180,12 @@ RSpec.describe Repository do
expect(repository.license).to be_nil
end
- it 'returns nil when the content is not recognizable' do
+ it 'returns other when the content is not recognizable' do
+ license = Licensee::License.new('other')
repository.create_file(user, 'LICENSE', 'Gitlab B.V.',
message: 'Add LICENSE', branch_name: 'master')
- expect(repository.license).to be_nil
+ expect(repository.license).to eq(license)
end
it 'returns the license' do
@@ -1938,7 +1933,6 @@ RSpec.describe Repository do
expect(repository).to receive(:expire_method_caches).with([
:size,
:commit_count,
- :rendered_readme,
:readme_path,
:contribution_guide,
:changelog,
@@ -1955,8 +1949,8 @@ RSpec.describe Repository do
:root_ref,
:merged_branch_names,
:has_visible_content?,
- :issue_template_names,
- :merge_request_template_names,
+ :issue_template_names_by_category,
+ :merge_request_template_names_by_category,
:user_defined_metrics_dashboard_paths,
:xcode_project?,
:has_ambiguous_refs?
@@ -2314,14 +2308,6 @@ RSpec.describe Repository do
expect(repository.readme).to be_nil
end
end
-
- context 'when a README exists' do
- let(:project) { create(:project, :repository) }
-
- it 'returns the README' do
- expect(repository.readme).to be_an_instance_of(ReadmeBlob)
- end
- end
end
end
@@ -2527,9 +2513,8 @@ RSpec.describe Repository do
describe '#refresh_method_caches' do
it 'refreshes the caches of the given types' do
expect(repository).to receive(:expire_method_caches)
- .with(%i(rendered_readme readme_path license_blob license_key license))
+ .with(%i(readme_path license_blob license_key license))
- expect(repository).to receive(:rendered_readme)
expect(repository).to receive(:readme_path)
expect(repository).to receive(:license_blob)
expect(repository).to receive(:license_key)
@@ -3049,4 +3034,51 @@ RSpec.describe Repository do
end
end
end
+
+ describe '.pick_storage_shard', :request_store do
+ before do
+ storages = {
+ 'default' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/repositories'),
+ 'picked' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/repositories')
+ }
+
+ allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+ Gitlab::CurrentSettings.current_application_settings
+
+ update_storages({ 'picked' => 0, 'default' => 100 })
+ end
+
+ context 'when expire is false' do
+ it 'does not expire existing repository storage value' do
+ previous_storage = described_class.pick_storage_shard
+ expect(previous_storage).to eq('default')
+ expect(Gitlab::CurrentSettings).not_to receive(:expire_current_application_settings)
+
+ update_storages({ 'picked' => 100, 'default' => 0 })
+
+ new_storage = described_class.pick_storage_shard(expire: false)
+ expect(new_storage).to eq(previous_storage)
+ end
+ end
+
+ context 'when expire is true' do
+ it 'expires existing repository storage value' do
+ previous_storage = described_class.pick_storage_shard
+ expect(previous_storage).to eq('default')
+ expect(Gitlab::CurrentSettings).to receive(:expire_current_application_settings).and_call_original
+
+ update_storages({ 'picked' => 100, 'default' => 0 })
+
+ new_storage = described_class.pick_storage_shard(expire: true)
+ expect(new_storage).to eq('picked')
+ end
+ end
+
+ def update_storages(storage_hash)
+ settings = ApplicationSetting.last
+ settings.repository_storages_weighted = storage_hash
+ settings.save!
+ end
+ end
end
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 04b3920cd6c..9ffefd4bbf7 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -39,35 +39,29 @@ RSpec.describe Service do
end
end
- context 'with an existing service template' do
- before do
+ context 'with existing services' do
+ before_all do
create(:service, :template)
+ create(:service, :instance)
+ create(:service, project: project)
+ create(:service, group: group, project: nil)
end
- it 'validates only one service template per type' do
+ it 'allows only one service template per type' do
expect(build(:service, :template)).to be_invalid
end
- end
- context 'with an existing instance service' do
- before do
- create(:service, :instance)
- end
-
- it 'validates only one service instance per type' do
+ it 'allows only one instance service per type' do
expect(build(:service, :instance)).to be_invalid
end
- end
- it 'validates uniqueness of type and project_id on create' do
- expect(create(:service, project: project, type: 'Service')).to be_valid
- expect(build(:service, project: project, type: 'Service').valid?(:create)).to eq(false)
- expect(build(:service, project: project, type: 'Service').valid?(:update)).to eq(true)
- end
+ it 'allows only one project service per type' do
+ expect(build(:service, project: project)).to be_invalid
+ end
- it 'validates uniqueness of type and group_id' do
- expect(create(:service, group_id: group.id, project_id: nil, type: 'Service')).to be_valid
- expect(build(:service, group_id: group.id, project_id: nil, type: 'Service')).to be_invalid
+ it 'allows only one group service per type' do
+ expect(build(:service, group: group, project: nil)).to be_invalid
+ end
end
end
@@ -753,38 +747,6 @@ RSpec.describe Service do
end
end
- describe "callbacks" do
- let!(:service) do
- RedmineService.new(
- project: project,
- active: true,
- properties: {
- project_url: 'http://redmine/projects/project_name_in_redmine',
- issues_url: "http://redmine/#{project.id}/project_name_in_redmine/:id",
- new_issue_url: 'http://redmine/projects/project_name_in_redmine/issues/new'
- }
- )
- end
-
- describe "on create" do
- it "updates the has_external_issue_tracker boolean" do
- expect do
- service.save!
- end.to change { service.project.has_external_issue_tracker }.from(false).to(true)
- end
- end
-
- describe "on update" do
- it "updates the has_external_issue_tracker boolean" do
- service.save!
-
- expect do
- service.update(active: false)
- end.to change { service.project.has_external_issue_tracker }.from(true).to(false)
- end
- end
- end
-
describe '#api_field_names' do
let(:fake_service) do
Class.new(Service) do
@@ -864,20 +826,6 @@ RSpec.describe Service do
end
end
- describe '#external_issue_tracker?' do
- where(:category, :active, :result) do
- :issue_tracker | true | true
- :issue_tracker | false | false
- :common | true | false
- end
-
- with_them do
- it 'returns the right result' do
- expect(build(:service, category: category, active: active).external_issue_tracker?).to eq(result)
- end
- end
- end
-
describe '#external_wiki?' do
where(:type, :active, :result) do
'ExternalWikiService' | true | true
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index 68d183d5d55..623767d19e0 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -630,14 +630,10 @@ RSpec.describe Snippet do
subject { snippet.repository_storage }
before do
- expect_next_instance_of(ApplicationSetting) do |instance|
- expect(instance).to receive(:pick_repository_storage).and_return('picked')
- end
+ expect(Repository).to receive(:pick_storage_shard).and_return('picked')
end
it 'returns repository storage from ApplicationSetting' do
- expect(described_class).to receive(:pick_repository_storage).and_call_original
-
expect(subject).to eq 'picked'
end
diff --git a/spec/models/terraform/state_spec.rb b/spec/models/terraform/state_spec.rb
index ed311314086..1319e2adb03 100644
--- a/spec/models/terraform/state_spec.rb
+++ b/spec/models/terraform/state_spec.rb
@@ -8,8 +8,11 @@ RSpec.describe Terraform::State do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:locked_by_user).class_name('User') }
+ it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_presence_of(:project_id) }
+ it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) }
+
describe 'scopes' do
describe '.ordered_by_name' do
let_it_be(:project) { create(:project) }
@@ -25,6 +28,15 @@ RSpec.describe Terraform::State do
it { expect(subject.map(&:name)).to eq(names.sort) }
end
+
+ describe '.with_name' do
+ let_it_be(:matching_name) { create(:terraform_state, name: 'matching-name') }
+ let_it_be(:other_name) { create(:terraform_state, name: 'other-name') }
+
+ subject { described_class.with_name(matching_name.name) }
+
+ it { is_expected.to contain_exactly(matching_name) }
+ end
end
describe '#destroy' do
diff --git a/spec/models/terraform/state_version_spec.rb b/spec/models/terraform/state_version_spec.rb
index 97ac77d5e7b..ac2e8d167b3 100644
--- a/spec/models/terraform/state_version_spec.rb
+++ b/spec/models/terraform/state_version_spec.rb
@@ -24,6 +24,24 @@ RSpec.describe Terraform::StateVersion do
it { expect(subject.map(&:version)).to eq(versions.sort.reverse) }
end
+
+ describe '.with_files_stored_locally' do
+ subject { described_class.with_files_stored_locally }
+
+ it 'includes states with local storage' do
+ create_list(:terraform_state_version, 5)
+
+ expect(subject).to have_attributes(count: 5)
+ end
+
+ it 'excludes states without local storage' do
+ stub_terraform_state_object_storage
+
+ create_list(:terraform_state_version, 5)
+
+ expect(subject).to have_attributes(count: 0)
+ end
+ end
end
context 'file storage' do
diff --git a/spec/models/token_with_iv_spec.rb b/spec/models/token_with_iv_spec.rb
new file mode 100644
index 00000000000..8dbccc19217
--- /dev/null
+++ b/spec/models/token_with_iv_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe TokenWithIv do
+ describe 'validations' do
+ it { is_expected.to validate_presence_of :hashed_token }
+ it { is_expected.to validate_presence_of :iv }
+ it { is_expected.to validate_presence_of :hashed_plaintext_token }
+ end
+
+ describe '.find_by_hashed_token' do
+ it 'only includes matching record' do
+ matching_record = create(:token_with_iv, hashed_token: ::Digest::SHA256.digest('hashed-token'))
+ create(:token_with_iv)
+
+ expect(described_class.find_by_hashed_token('hashed-token')).to eq(matching_record)
+ end
+ end
+
+ describe '.find_by_plaintext_token' do
+ it 'only includes matching record' do
+ matching_record = create(:token_with_iv, hashed_plaintext_token: ::Digest::SHA256.digest('hashed-token'))
+ create(:token_with_iv)
+
+ expect(described_class.find_by_plaintext_token('hashed-token')).to eq(matching_record)
+ end
+ end
+end
diff --git a/spec/models/u2f_registration_spec.rb b/spec/models/u2f_registration_spec.rb
new file mode 100644
index 00000000000..1f2e4d1e447
--- /dev/null
+++ b/spec/models/u2f_registration_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe U2fRegistration do
+ let_it_be(:user) { create(:user) }
+ let(:u2f_registration) do
+ device = U2F::FakeU2F.new(FFaker::BaconIpsum.characters(5))
+ create(:u2f_registration, name: 'u2f_device',
+ user: user,
+ certificate: Base64.strict_encode64(device.cert_raw),
+ key_handle: U2F.urlsafe_encode64(device.key_handle_raw),
+ public_key: Base64.strict_encode64(device.origin_public_key_raw))
+ end
+
+ describe 'callbacks' do
+ describe '#create_webauthn_registration' do
+ it 'creates webauthn registration' do
+ u2f_registration.save!
+
+ webauthn_registration = WebauthnRegistration.where(u2f_registration_id: u2f_registration.id)
+ expect(webauthn_registration).to exist
+ end
+
+ it 'logs error' do
+ allow(Gitlab::Auth::U2fWebauthnConverter).to receive(:new).and_raise('boom!')
+ expect(Gitlab::AppJsonLogger).to(
+ receive(:error).with(a_hash_including(event: 'u2f_migration',
+ error: 'RuntimeError',
+ message: 'U2F to WebAuthn conversion failed'))
+ )
+
+ u2f_registration.save!
+ end
+ end
+ end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 0935d3576a4..860c015e166 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -2477,7 +2477,7 @@ RSpec.describe User do
it 'is false if avatar is html page' do
user.update_attribute(:avatar, 'uploads/avatar.html')
- expect(user.avatar_type).to eq(['file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico'])
+ expect(user.avatar_type).to eq(['file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico, webp'])
end
end
@@ -2831,6 +2831,79 @@ RSpec.describe User do
end
end
+ describe '#following?' do
+ it 'check if following another user' do
+ user = create :user
+ followee1 = create :user
+
+ expect(user.follow(followee1)).to be_truthy
+
+ expect(user.following?(followee1)).to be_truthy
+
+ expect(user.unfollow(followee1)).to be_truthy
+
+ expect(user.following?(followee1)).to be_falsey
+ end
+ end
+
+ describe '#follow' do
+ it 'follow another user' do
+ user = create :user
+ followee1 = create :user
+ followee2 = create :user
+
+ expect(user.followees).to be_empty
+
+ expect(user.follow(followee1)).to be_truthy
+ expect(user.follow(followee1)).to be_falsey
+
+ expect(user.followees).to contain_exactly(followee1)
+
+ expect(user.follow(followee2)).to be_truthy
+ expect(user.follow(followee2)).to be_falsey
+
+ expect(user.followees).to contain_exactly(followee1, followee2)
+ end
+
+ it 'follow itself is not possible' do
+ user = create :user
+
+ expect(user.followees).to be_empty
+
+ expect(user.follow(user)).to be_falsey
+
+ expect(user.followees).to be_empty
+ end
+ end
+
+ describe '#unfollow' do
+ it 'unfollow another user' do
+ user = create :user
+ followee1 = create :user
+ followee2 = create :user
+
+ expect(user.followees).to be_empty
+
+ expect(user.follow(followee1)).to be_truthy
+ expect(user.follow(followee1)).to be_falsey
+
+ expect(user.follow(followee2)).to be_truthy
+ expect(user.follow(followee2)).to be_falsey
+
+ expect(user.followees).to contain_exactly(followee1, followee2)
+
+ expect(user.unfollow(followee1)).to be_truthy
+ expect(user.unfollow(followee1)).to be_falsey
+
+ expect(user.followees).to contain_exactly(followee2)
+
+ expect(user.unfollow(followee2)).to be_truthy
+ expect(user.unfollow(followee2)).to be_falsey
+
+ expect(user.followees).to be_empty
+ end
+ end
+
describe '.find_by_private_commit_email' do
context 'with email' do
let_it_be(:user) { create(:user) }
diff --git a/spec/models/user_status_spec.rb b/spec/models/user_status_spec.rb
index 2c0664bd165..51dd91149cc 100644
--- a/spec/models/user_status_spec.rb
+++ b/spec/models/user_status_spec.rb
@@ -17,4 +17,34 @@ RSpec.describe UserStatus do
expect { status.user.destroy }.to change { described_class.count }.from(1).to(0)
end
+
+ describe '#clear_status_after=' do
+ it 'sets clear_status_at' do
+ status = build(:user_status)
+
+ freeze_time do
+ status.clear_status_after = '8_hours'
+
+ expect(status.clear_status_at).to be_like_time(8.hours.from_now)
+ end
+ end
+
+ it 'unsets clear_status_at' do
+ status = build(:user_status, clear_status_at: 8.hours.from_now)
+
+ status.clear_status_after = nil
+
+ expect(status.clear_status_at).to be_nil
+ end
+
+ context 'when unknown clear status is given' do
+ it 'unsets clear_status_at' do
+ status = build(:user_status, clear_status_at: 8.hours.from_now)
+
+ status.clear_status_after = 'unknown'
+
+ expect(status.clear_status_at).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 8bd4a463f87..6ba3ab6aace 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -468,6 +468,49 @@ RSpec.describe ProjectPolicy do
end
end
+ context "project bots" do
+ let(:project_bot) { create(:user, :project_bot) }
+ let(:user) { create(:user) }
+
+ context "project_bot_access" do
+ context "when regular user and part of the project" do
+ let(:current_user) { user }
+
+ before do
+ project.add_developer(user)
+ end
+
+ it { is_expected.not_to be_allowed(:project_bot_access)}
+ end
+
+ context "when project bot and not part of the project" do
+ let(:current_user) { project_bot }
+
+ it { is_expected.not_to be_allowed(:project_bot_access)}
+ end
+
+ context "when project bot and part of the project" do
+ let(:current_user) { project_bot }
+
+ before do
+ project.add_developer(project_bot)
+ end
+
+ it { is_expected.to be_allowed(:project_bot_access)}
+ end
+ end
+
+ context 'with resource access tokens' do
+ let(:current_user) { project_bot }
+
+ before do
+ project.add_maintainer(project_bot)
+ end
+
+ it { is_expected.not_to be_allowed(:admin_resource_access_tokens)}
+ end
+ end
+
describe 'read_prometheus_alerts' do
context 'with admin' do
let(:current_user) { admin }
@@ -822,6 +865,28 @@ RSpec.describe ProjectPolicy do
end
end
+ context 'security configuration feature' do
+ %w(guest reporter).each do |role|
+ context role do
+ let(:current_user) { send(role) }
+
+ it 'prevents reading security configuration' do
+ expect_disallowed(:read_security_configuration)
+ end
+ end
+ end
+
+ %w(developer maintainer owner).each do |role|
+ context role do
+ let(:current_user) { send(role) }
+
+ it 'allows reading security configuration' do
+ expect_allowed(:read_security_configuration)
+ end
+ end
+ end
+ end
+
describe 'design permissions' do
let(:current_user) { guest }
diff --git a/spec/presenters/ci/build_runner_presenter_spec.rb b/spec/presenters/ci/build_runner_presenter_spec.rb
index f78ad38f4e8..43b677483ce 100644
--- a/spec/presenters/ci/build_runner_presenter_spec.rb
+++ b/spec/presenters/ci/build_runner_presenter_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe Ci::BuildRunnerPresenter do
describe '#artifacts' do
context "when option contains archive-type artifacts" do
- let(:build) { create(:ci_build, options: { artifacts: archive } ) }
+ let(:build) { create(:ci_build, options: { artifacts: archive }) }
it 'presents correct hash' do
expect(presenter.artifacts.first).to include(archive_expectation)
@@ -196,16 +196,6 @@ RSpec.describe Ci::BuildRunnerPresenter do
expect(subject[0]).to match(/^\+[0-9a-f]{40}:refs\/pipelines\/[0-9]+$/)
end
- context 'when the scalability_ci_fetch_sha feature flag is disabled' do
- before do
- stub_feature_flags(scalability_ci_fetch_sha: false)
- end
-
- it 'fetches the ref by name' do
- expect(subject[0]).to eq("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}")
- end
- end
-
context 'when ref is tag' do
let(:build) { create(:ci_build, :tag) }
@@ -259,7 +249,7 @@ RSpec.describe Ci::BuildRunnerPresenter do
it 'returns the correct refspecs' do
is_expected.to contain_exactly("+#{pipeline.sha}:refs/pipelines/#{pipeline.id}",
- "+refs/heads/#{build.ref}:refs/remotes/origin/#{build.ref}")
+ "+refs/heads/#{build.ref}:refs/remotes/origin/#{build.ref}")
end
end
end
diff --git a/spec/presenters/ci/pipeline_artifacts/code_quality_mr_diff_presenter_spec.rb b/spec/presenters/ci/pipeline_artifacts/code_quality_mr_diff_presenter_spec.rb
new file mode 100644
index 00000000000..06d5422eed3
--- /dev/null
+++ b/spec/presenters/ci/pipeline_artifacts/code_quality_mr_diff_presenter_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::PipelineArtifacts::CodeQualityMrDiffPresenter do
+ let(:pipeline_artifact) { create(:ci_pipeline_artifact, :with_codequality_mr_diff_report) }
+
+ subject(:presenter) { described_class.new(pipeline_artifact) }
+
+ describe '#for_files' do
+ subject(:quality_data) { presenter.for_files(filenames) }
+
+ context 'when code quality has data' do
+ context 'when filenames is empty' do
+ let(:filenames) { %w() }
+
+ it 'returns hash without quality' do
+ expect(quality_data).to match(files: {})
+ end
+ end
+
+ context 'when filenames do not match code quality data' do
+ let(:filenames) { %w(demo.rb) }
+
+ it 'returns hash without quality' do
+ expect(quality_data).to match(files: {})
+ end
+ end
+
+ context 'when filenames matches code quality data' do
+ context 'when asking for one filename' do
+ let(:filenames) { %w(file_a.rb) }
+
+ it 'returns quality for the given filename' do
+ expect(quality_data).to match(
+ files: {
+ "file_a.rb" => [
+ { line: 10, description: "Avoid parameter lists longer than 5 parameters. [12/5]", severity: "major" },
+ { line: 10, description: "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.", severity: "minor" }
+ ]
+ }
+ )
+ end
+ end
+
+ context 'when asking for multiple filenames' do
+ let(:filenames) { %w(file_a.rb file_b.rb) }
+
+ it 'returns quality for the given filenames' do
+ expect(quality_data).to match(
+ files: {
+ "file_a.rb" => [
+ { line: 10, description: "Avoid parameter lists longer than 5 parameters. [12/5]", severity: "major" },
+ { line: 10, description: "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.", severity: "minor" }
+ ],
+ "file_b.rb" => [
+ { line: 10, description: "This cop checks for methods with too many parameters.\nThe maximum number of parameters is configurable.\nKeyword arguments can optionally be excluded from the total count.", severity: "minor" }
+ ]
+ }
+ )
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/presenters/gitlab/whats_new/item_presenter_spec.rb b/spec/presenters/gitlab/whats_new/item_presenter_spec.rb
deleted file mode 100644
index 9b04741aa60..00000000000
--- a/spec/presenters/gitlab/whats_new/item_presenter_spec.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::WhatsNew::ItemPresenter do
- let(:present) { Gitlab::WhatsNew::ItemPresenter.present(item) }
- let(:item) { { "packages" => %w(Core Starter Premium Ultimate) } }
- let(:gitlab_com) { true }
-
- before do
- allow(Gitlab).to receive(:com?).and_return(gitlab_com)
- end
-
- describe '.present' do
- context 'when on Gitlab.com' do
- it 'transforms package names to gitlab.com friendly package names' do
- expect(present).to eq({ "packages" => %w(Free Bronze Silver Gold) })
- end
- end
-
- context 'when not on Gitlab.com' do
- let(:gitlab_com) { false }
-
- it 'does not transform package names' do
- expect(present).to eq({ "packages" => %w(Core Starter Premium Ultimate) })
- end
- end
- end
-end
diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb
index a9050c233af..98bcbd8384b 100644
--- a/spec/presenters/project_presenter_spec.rb
+++ b/spec/presenters/project_presenter_spec.rb
@@ -350,7 +350,7 @@ RSpec.describe ProjectPresenter do
is_link: false,
label: a_string_including("New file"),
link: presenter.project_new_blob_path(project, 'master'),
- class_modifier: 'missing'
+ class_modifier: 'dashed'
)
end
diff --git a/spec/requests/api/api_spec.rb b/spec/requests/api/api_spec.rb
index 9fd30213133..8bd6049e6fa 100644
--- a/spec/requests/api/api_spec.rb
+++ b/spec/requests/api/api_spec.rb
@@ -36,6 +36,12 @@ RSpec.describe API::API do
expect(response).to have_gitlab_http_status(:ok)
end
+ it 'does authorize user for head request' do
+ head api('/groups', personal_access_token: token)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
it 'does not authorize user for revoked token' do
revoked = create(:personal_access_token, :revoked, user: user, scopes: [:read_api])
@@ -126,4 +132,34 @@ RSpec.describe API::API do
get(api('/users'))
end
end
+
+ describe 'supported content-types' do
+ context 'GET /user/:id.txt' do
+ let_it_be(:user) { create(:user) }
+
+ subject { get api("/users/#{user.id}.txt", user) }
+
+ it 'returns application/json' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.media_type).to eq('application/json')
+ expect(response.body).to include('{"id":')
+ end
+
+ context 'when api_always_use_application_json is disabled' do
+ before do
+ stub_feature_flags(api_always_use_application_json: false)
+ end
+
+ it 'returns text/plain' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.media_type).to eq('text/plain')
+ expect(response.body).to include('#<API::Entities::User:')
+ end
+ end
+ end
+ end
end
diff --git a/spec/requests/api/applications_spec.rb b/spec/requests/api/applications_spec.rb
index 63fbf6e32dd..ca09f5524ca 100644
--- a/spec/requests/api/applications_spec.rb
+++ b/spec/requests/api/applications_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe API::Applications, :api do
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response).to be_a Hash
- expect(json_response['message']['redirect_uri'][0]).to eq('must be an absolute URI.')
+ expect(json_response['message']['redirect_uri'][0]).to eq('must be a valid URI.')
end
it 'does not allow creating an application with a forbidden URI format' do
diff --git a/spec/requests/api/ci/pipelines_spec.rb b/spec/requests/api/ci/pipelines_spec.rb
index 767b5704851..a9afbd8bd72 100644
--- a/spec/requests/api/ci/pipelines_spec.rb
+++ b/spec/requests/api/ci/pipelines_spec.rb
@@ -312,7 +312,7 @@ RSpec.describe API::Ci::Pipelines do
let(:query) { {} }
let(:api_user) { user }
let_it_be(:job) do
- create(:ci_build, :success, pipeline: pipeline,
+ create(:ci_build, :success, name: 'build', pipeline: pipeline,
artifacts_expire_at: 1.day.since)
end
@@ -405,6 +405,38 @@ RSpec.describe API::Ci::Pipelines do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), params: query
end.not_to exceed_all_query_limit(control_count)
end
+
+ context 'pipeline has retried jobs' do
+ before_all do
+ job.update!(retried: true)
+ end
+
+ let_it_be(:successor) { create(:ci_build, :success, name: 'build', pipeline: pipeline) }
+
+ it 'does not return retried jobs by default' do
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ end
+
+ context 'when include_retried is false' do
+ let(:query) { { include_retried: false } }
+
+ it 'does not return retried jobs' do
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ end
+ end
+
+ context 'when include_retried is true' do
+ let(:query) { { include_retried: true } }
+
+ it 'returns retried jobs' do
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response[0]['name']).to eq(json_response[1]['name'])
+ end
+ end
+ end
end
context 'no pipeline is found' do
diff --git a/spec/requests/api/debian_group_packages_spec.rb b/spec/requests/api/debian_group_packages_spec.rb
index 8a05d20fb33..9d63d675a02 100644
--- a/spec/requests/api/debian_group_packages_spec.rb
+++ b/spec/requests/api/debian_group_packages_spec.rb
@@ -6,32 +6,32 @@ RSpec.describe API::DebianGroupPackages do
include WorkhorseHelpers
include_context 'Debian repository shared context', :group do
- describe 'GET groups/:id/-/packages/debian/dists/*distribution/Release.gpg' do
- let(:url) { "/groups/#{group.id}/-/packages/debian/dists/#{distribution}/Release.gpg" }
+ describe 'GET groups/:id/packages/debian/dists/*distribution/Release.gpg' do
+ let(:url) { "/groups/#{group.id}/packages/debian/dists/#{distribution}/Release.gpg" }
it_behaves_like 'Debian group repository GET endpoint', :not_found, nil
end
- describe 'GET groups/:id/-/packages/debian/dists/*distribution/Release' do
- let(:url) { "/groups/#{group.id}/-/packages/debian/dists/#{distribution}/Release" }
+ describe 'GET groups/:id/packages/debian/dists/*distribution/Release' do
+ let(:url) { "/groups/#{group.id}/packages/debian/dists/#{distribution}/Release" }
it_behaves_like 'Debian group repository GET endpoint', :success, 'TODO Release'
end
- describe 'GET groups/:id/-/packages/debian/dists/*distribution/InRelease' do
- let(:url) { "/groups/#{group.id}/-/packages/debian/dists/#{distribution}/InRelease" }
+ describe 'GET groups/:id/packages/debian/dists/*distribution/InRelease' do
+ let(:url) { "/groups/#{group.id}/packages/debian/dists/#{distribution}/InRelease" }
it_behaves_like 'Debian group repository GET endpoint', :not_found, nil
end
- describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/Packages' do
- let(:url) { "/groups/#{group.id}/-/packages/debian/dists/#{distribution}/#{component}/binary-#{architecture}/Packages" }
+ describe 'GET groups/:id/packages/debian/dists/*distribution/:component/binary-:architecture/Packages' do
+ let(:url) { "/groups/#{group.id}/packages/debian/dists/#{distribution}/#{component}/binary-#{architecture}/Packages" }
it_behaves_like 'Debian group repository GET endpoint', :success, 'TODO Packages'
end
- describe 'GET groups/:id/-/packages/debian/pool/:component/:letter/:source_package/:file_name' do
- let(:url) { "/groups/#{group.id}/-/packages/debian/pool/#{component}/#{letter}/#{source_package}/#{package_name}_#{package_version}_#{architecture}.deb" }
+ describe 'GET groups/:id/packages/debian/pool/:component/:letter/:source_package/:file_name' do
+ let(:url) { "/groups/#{group.id}/packages/debian/pool/#{component}/#{letter}/#{source_package}/#{package_name}_#{package_version}_#{architecture}.deb" }
it_behaves_like 'Debian group repository GET endpoint', :success, 'TODO File'
end
diff --git a/spec/requests/api/debian_project_packages_spec.rb b/spec/requests/api/debian_project_packages_spec.rb
index 663b69b1b76..4941f2a77f4 100644
--- a/spec/requests/api/debian_project_packages_spec.rb
+++ b/spec/requests/api/debian_project_packages_spec.rb
@@ -6,46 +6,46 @@ RSpec.describe API::DebianProjectPackages do
include WorkhorseHelpers
include_context 'Debian repository shared context', :project do
- describe 'GET projects/:id/-/packages/debian/dists/*distribution/Release.gpg' do
- let(:url) { "/projects/#{project.id}/-/packages/debian/dists/#{distribution}/Release.gpg" }
+ describe 'GET projects/:id/packages/debian/dists/*distribution/Release.gpg' do
+ let(:url) { "/projects/#{project.id}/packages/debian/dists/#{distribution}/Release.gpg" }
it_behaves_like 'Debian project repository GET endpoint', :not_found, nil
end
- describe 'GET projects/:id/-/packages/debian/dists/*distribution/Release' do
- let(:url) { "/projects/#{project.id}/-/packages/debian/dists/#{distribution}/Release" }
+ describe 'GET projects/:id/packages/debian/dists/*distribution/Release' do
+ let(:url) { "/projects/#{project.id}/packages/debian/dists/#{distribution}/Release" }
it_behaves_like 'Debian project repository GET endpoint', :success, 'TODO Release'
end
- describe 'GET projects/:id/-/packages/debian/dists/*distribution/InRelease' do
- let(:url) { "/projects/#{project.id}/-/packages/debian/dists/#{distribution}/InRelease" }
+ describe 'GET projects/:id/packages/debian/dists/*distribution/InRelease' do
+ let(:url) { "/projects/#{project.id}/packages/debian/dists/#{distribution}/InRelease" }
it_behaves_like 'Debian project repository GET endpoint', :not_found, nil
end
- describe 'GET projects/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/Packages' do
- let(:url) { "/projects/#{project.id}/-/packages/debian/dists/#{distribution}/#{component}/binary-#{architecture}/Packages" }
+ describe 'GET projects/:id/packages/debian/dists/*distribution/:component/binary-:architecture/Packages' do
+ let(:url) { "/projects/#{project.id}/packages/debian/dists/#{distribution}/#{component}/binary-#{architecture}/Packages" }
it_behaves_like 'Debian project repository GET endpoint', :success, 'TODO Packages'
end
- describe 'GET projects/:id/-/packages/debian/pool/:component/:letter/:source_package/:file_name' do
- let(:url) { "/projects/#{project.id}/-/packages/debian/pool/#{component}/#{letter}/#{source_package}/#{package_name}_#{package_version}_#{architecture}.deb" }
+ describe 'GET projects/:id/packages/debian/pool/:component/:letter/:source_package/:file_name' do
+ let(:url) { "/projects/#{project.id}/packages/debian/pool/#{component}/#{letter}/#{source_package}/#{package_name}_#{package_version}_#{architecture}.deb" }
it_behaves_like 'Debian project repository GET endpoint', :success, 'TODO File'
end
- describe 'PUT projects/:id/-/packages/debian/incoming/:file_name' do
+ describe 'PUT projects/:id/packages/debian/:file_name' do
let(:method) { :put }
- let(:url) { "/projects/#{project.id}/-/packages/debian/incoming/#{file_name}" }
+ let(:url) { "/projects/#{project.id}/packages/debian/#{file_name}" }
it_behaves_like 'Debian project repository PUT endpoint', :created, nil
end
- describe 'PUT projects/:id/-/packages/debian/incoming/:file_name/authorize' do
+ describe 'PUT projects/:id/packages/debian/:file_name/authorize' do
let(:method) { :put }
- let(:url) { "/projects/#{project.id}/-/packages/debian/incoming/#{file_name}/authorize" }
+ let(:url) { "/projects/#{project.id}/packages/debian/#{file_name}/authorize" }
it_behaves_like 'Debian project repository PUT endpoint', :created, nil, is_authorize: true
end
diff --git a/spec/requests/api/deploy_tokens_spec.rb b/spec/requests/api/deploy_tokens_spec.rb
index 8ec4f888e2e..7a31ff725c8 100644
--- a/spec/requests/api/deploy_tokens_spec.rb
+++ b/spec/requests/api/deploy_tokens_spec.rb
@@ -10,24 +10,12 @@ RSpec.describe API::DeployTokens do
let!(:deploy_token) { create(:deploy_token, projects: [project]) }
let!(:group_deploy_token) { create(:deploy_token, :group, groups: [group]) }
- shared_examples 'with feature flag disabled' do
- context 'disabled feature flag' do
- before do
- stub_feature_flags(deploy_tokens_api: false)
- end
-
- it { is_expected.to have_gitlab_http_status(:service_unavailable) }
- end
- end
-
describe 'GET /deploy_tokens' do
subject do
get api('/deploy_tokens', user)
response
end
- it_behaves_like 'with feature flag disabled'
-
context 'when unauthenticated' do
let(:user) { nil }
@@ -81,8 +69,6 @@ RSpec.describe API::DeployTokens do
project.add_maintainer(user)
end
- it_behaves_like 'with feature flag disabled'
-
it { is_expected.to have_gitlab_http_status(:ok) }
it 'returns all deploy tokens for the project' do
@@ -128,8 +114,6 @@ RSpec.describe API::DeployTokens do
group.add_maintainer(user)
end
- it_behaves_like 'with feature flag disabled'
-
it { is_expected.to have_gitlab_http_status(:ok) }
it 'returns all deploy tokens for the group' do
diff --git a/spec/requests/api/events_spec.rb b/spec/requests/api/events_spec.rb
index 6a8d5f91abd..110d6e2f99e 100644
--- a/spec/requests/api/events_spec.rb
+++ b/spec/requests/api/events_spec.rb
@@ -55,6 +55,12 @@ RSpec.describe API::Events do
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
end
+
+ it 'returns "200" response on head request' do
+ head api('/events?action=closed&target_type=issue&after=2016-12-1&before=2016-12-31', personal_access_token: token)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
context 'when the requesting token does not have "read_user" or "api" scope' do
diff --git a/spec/requests/api/generic_packages_spec.rb b/spec/requests/api/generic_packages_spec.rb
index d162d288129..a47be1ead9c 100644
--- a/spec/requests/api/generic_packages_spec.rb
+++ b/spec/requests/api/generic_packages_spec.rb
@@ -4,6 +4,9 @@ require 'spec_helper'
RSpec.describe API::GenericPackages do
include HttpBasicAuthHelpers
+ using RSpec::Parameterized::TableSyntax
+
+ include_context 'workhorse headers'
let_it_be(:personal_access_token) { create(:personal_access_token) }
let_it_be(:project, reload: true) { create(:project) }
@@ -13,8 +16,6 @@ RSpec.describe API::GenericPackages do
let_it_be(:project_deploy_token_ro) { create(:project_deploy_token, deploy_token: deploy_token_ro, project: project) }
let_it_be(:deploy_token_wo) { create(:deploy_token, read_package_registry: false, write_package_registry: true) }
let_it_be(:project_deploy_token_wo) { create(:project_deploy_token, deploy_token: deploy_token_wo, project: project) }
- let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
- let(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
let(:user) { personal_access_token.user }
let(:ci_build) { create(:ci_build, :running, user: user) }
@@ -76,8 +77,6 @@ RSpec.describe API::GenericPackages do
describe 'PUT /api/v4/projects/:id/packages/generic/:package_name/:package_version/:file_name/authorize' do
context 'with valid project' do
- using RSpec::Parameterized::TableSyntax
-
where(:project_visibility, :user_role, :member?, :authenticate_with, :expected_status) do
'PUBLIC' | :developer | true | :personal_access_token | :success
'PUBLIC' | :guest | true | :personal_access_token | :forbidden
@@ -130,7 +129,7 @@ RSpec.describe API::GenericPackages do
end
it "responds with #{params[:expected_status]}" do
- authorize_upload_file(workhorse_header.merge(auth_header))
+ authorize_upload_file(workhorse_headers.merge(auth_header))
expect(response).to have_gitlab_http_status(expected_status)
end
@@ -145,7 +144,7 @@ RSpec.describe API::GenericPackages do
with_them do
it "responds with #{params[:expected_status]}" do
- authorize_upload_file(workhorse_header.merge(deploy_token_auth_header))
+ authorize_upload_file(workhorse_headers.merge(deploy_token_auth_header))
expect(response).to have_gitlab_http_status(expected_status)
end
@@ -163,7 +162,7 @@ RSpec.describe API::GenericPackages do
end
with_them do
- subject { authorize_upload_file(workhorse_header.merge(personal_access_token_header), param_name => param_value) }
+ subject { authorize_upload_file(workhorse_headers.merge(personal_access_token_header), param_name => param_value) }
it_behaves_like 'secure endpoint'
end
@@ -174,7 +173,7 @@ RSpec.describe API::GenericPackages do
stub_feature_flags(generic_packages: false)
project.add_developer(user)
- authorize_upload_file(workhorse_header.merge(personal_access_token_header))
+ authorize_upload_file(workhorse_headers.merge(personal_access_token_header))
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -194,8 +193,6 @@ RSpec.describe API::GenericPackages do
let(:params) { { file: file_upload } }
context 'authentication' do
- using RSpec::Parameterized::TableSyntax
-
where(:project_visibility, :user_role, :member?, :authenticate_with, :expected_status) do
'PUBLIC' | :guest | true | :personal_access_token | :forbidden
'PUBLIC' | :guest | true | :user_basic_auth | :forbidden
@@ -242,7 +239,7 @@ RSpec.describe API::GenericPackages do
end
it "responds with #{params[:expected_status]}" do
- headers = workhorse_header.merge(auth_header)
+ headers = workhorse_headers.merge(auth_header)
upload_file(params, headers)
@@ -257,7 +254,7 @@ RSpec.describe API::GenericPackages do
with_them do
it "responds with #{params[:expected_status]}" do
- headers = workhorse_header.merge(deploy_token_auth_header)
+ headers = workhorse_headers.merge(deploy_token_auth_header)
upload_file(params, headers)
@@ -273,7 +270,7 @@ RSpec.describe API::GenericPackages do
shared_examples 'creates a package and package file' do
it 'creates a package and package file' do
- headers = workhorse_header.merge(auth_header)
+ headers = workhorse_headers.merge(auth_header)
expect { upload_file(params, headers) }
.to change { project.packages.generic.count }.by(1)
@@ -284,6 +281,7 @@ RSpec.describe API::GenericPackages do
package = project.packages.generic.last
expect(package.name).to eq('mypackage')
+ expect(package.status).to eq('default')
expect(package.version).to eq('0.0.1')
if should_set_build_info
@@ -296,6 +294,39 @@ RSpec.describe API::GenericPackages do
expect(package_file.file_name).to eq('myfile.tar.gz')
end
end
+
+ context 'with a status' do
+ context 'valid status' do
+ let(:params) { super().merge(status: 'hidden') }
+
+ it 'assigns the status to the package' do
+ headers = workhorse_headers.merge(auth_header)
+
+ upload_file(params, headers)
+
+ aggregate_failures do
+ expect(response).to have_gitlab_http_status(:created)
+
+ package = project.packages.find_by(name: 'mypackage')
+ expect(package).to be_hidden
+ end
+ end
+ end
+
+ context 'invalid status' do
+ let(:params) { super().merge(status: 'processing') }
+
+ it 'rejects the package' do
+ headers = workhorse_headers.merge(auth_header)
+
+ upload_file(params, headers)
+
+ aggregate_failures do
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+ end
+ end
end
context 'when valid personal access token is used' do
@@ -327,26 +358,26 @@ RSpec.describe API::GenericPackages do
end
context 'event tracking' do
- subject { upload_file(params, workhorse_header.merge(personal_access_token_header)) }
+ subject { upload_file(params, workhorse_headers.merge(personal_access_token_header)) }
it_behaves_like 'a gitlab tracking event', described_class.name, 'push_package'
end
it 'rejects request without a file from workhorse' do
- headers = workhorse_header.merge(personal_access_token_header)
+ headers = workhorse_headers.merge(personal_access_token_header)
upload_file({}, headers)
expect(response).to have_gitlab_http_status(:bad_request)
end
it 'rejects request without an auth token' do
- upload_file(params, workhorse_header)
+ upload_file(params, workhorse_headers)
expect(response).to have_gitlab_http_status(:unauthorized)
end
it 'rejects request without workhorse rewritten fields' do
- headers = workhorse_header.merge(personal_access_token_header)
+ headers = workhorse_headers.merge(personal_access_token_header)
upload_file(params, headers, send_rewritten_field: false)
expect(response).to have_gitlab_http_status(:bad_request)
@@ -357,7 +388,7 @@ RSpec.describe API::GenericPackages do
allow(uploaded_file).to receive(:size).and_return(project.actual_limits.generic_packages_max_file_size + 1)
end
- headers = workhorse_header.merge(personal_access_token_header)
+ headers = workhorse_headers.merge(personal_access_token_header)
upload_file(params, headers)
expect(response).to have_gitlab_http_status(:bad_request)
@@ -373,8 +404,6 @@ RSpec.describe API::GenericPackages do
end
context 'application security' do
- using RSpec::Parameterized::TableSyntax
-
where(:param_name, :param_value) do
:package_name | 'my-package/../'
:package_name | 'my-package%2f%2e%2e%2f'
@@ -383,7 +412,7 @@ RSpec.describe API::GenericPackages do
end
with_them do
- subject { upload_file(params, workhorse_header.merge(personal_access_token_header), param_name => param_value) }
+ subject { upload_file(params, workhorse_headers.merge(personal_access_token_header), param_name => param_value) }
it_behaves_like 'secure endpoint'
end
@@ -404,8 +433,6 @@ RSpec.describe API::GenericPackages do
end
describe 'GET /api/v4/projects/:id/packages/generic/:package_name/:package_version/:file_name' do
- using RSpec::Parameterized::TableSyntax
-
let_it_be(:package) { create(:generic_package, project: project) }
let_it_be(:package_file) { create(:package_file, :generic, package: package) }
@@ -527,8 +554,6 @@ RSpec.describe API::GenericPackages do
end
context 'application security' do
- using RSpec::Parameterized::TableSyntax
-
where(:param_name, :param_value) do
:package_name | 'my-package/../'
:package_name | 'my-package%2f%2e%2e%2f'
diff --git a/spec/requests/api/graphql/ci/application_setting_spec.rb b/spec/requests/api/graphql/ci/application_setting_spec.rb
new file mode 100644
index 00000000000..156ee550f16
--- /dev/null
+++ b/spec/requests/api/graphql/ci/application_setting_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'getting Application Settings' do
+ include GraphqlHelpers
+
+ let(:fields) do
+ <<~QUERY
+ #{all_graphql_fields_for('CiApplicationSettings', max_depth: 1)}
+ QUERY
+ end
+
+ let(:query) do
+ graphql_query_for(
+ 'ciApplicationSettings',
+ fields
+ )
+ end
+
+ let(:settings_data) { graphql_data['ciApplicationSettings'] }
+
+ context 'without admin permissions' do
+ let(:user) { create(:user) }
+
+ before do
+ post_graphql(query, current_user: user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ specify { expect(settings_data).to be nil }
+ end
+
+ context 'with admin permissions' do
+ let(:user) { create(:user, :admin) }
+
+ before do
+ post_graphql(query, current_user: user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'fetches the settings data' do
+ # assert against hash to ensure no additional fields are exposed
+ expect(settings_data).to match({ 'keepLatestArtifact' => Gitlab::CurrentSettings.current_application_settings.keep_latest_artifact })
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb b/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb
index db8a412e45c..99647d0fa3a 100644
--- a/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb
+++ b/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb
@@ -46,7 +46,7 @@ RSpec.describe 'Getting Ci Cd Setting' do
it 'fetches the settings data' do
expect(settings_data['mergePipelinesEnabled']).to eql project.ci_cd_settings.merge_pipelines_enabled?
expect(settings_data['mergeTrainsEnabled']).to eql project.ci_cd_settings.merge_trains_enabled?
- expect(settings_data['keepLatestArtifact']).to eql project.ci_keep_latest_artifact?
+ expect(settings_data['keepLatestArtifact']).to eql project.keep_latest_artifacts_available?
end
end
end
diff --git a/spec/requests/api/graphql/issue/issue_spec.rb b/spec/requests/api/graphql/issue/issue_spec.rb
index 42c8e0cc9c0..09e89f65882 100644
--- a/spec/requests/api/graphql/issue/issue_spec.rb
+++ b/spec/requests/api/graphql/issue/issue_spec.rb
@@ -24,6 +24,20 @@ RSpec.describe 'Query.issue(id)' do
end
end
+ it_behaves_like 'a noteable graphql type we can query' do
+ let(:noteable) { issue }
+ let(:project) { issue.project }
+ let(:path_to_noteable) { [:issue] }
+
+ before do
+ project.add_guest(current_user)
+ end
+
+ def query(fields)
+ graphql_query_for('issue', issue_params, fields)
+ end
+ end
+
context 'when the user does not have access to the issue' do
it 'returns nil' do
project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE)
diff --git a/spec/requests/api/graphql/mutations/alert_management/http_integration/update_spec.rb b/spec/requests/api/graphql/mutations/alert_management/http_integration/update_spec.rb
index bf7eb3d980c..18cbb7d8b00 100644
--- a/spec/requests/api/graphql/mutations/alert_management/http_integration/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/alert_management/http_integration/update_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Updating an existing HTTP Integration' do
include GraphqlHelpers
- let_it_be(:user) { create(:user) }
+ let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:integration) { create(:alert_management_http_integration, project: project) }
@@ -32,18 +32,8 @@ RSpec.describe 'Updating an existing HTTP Integration' do
let(:mutation_response) { graphql_mutation_response(:http_integration_update) }
before do
- project.add_maintainer(user)
+ project.add_maintainer(current_user)
end
- it 'updates the integration' do
- post_graphql_mutation(mutation, current_user: user)
-
- integration_response = mutation_response['integration']
-
- expect(response).to have_gitlab_http_status(:success)
- expect(integration_response['id']).to eq(GitlabSchema.id_from_object(integration).to_s)
- expect(integration_response['name']).to eq('Modified Name')
- expect(integration_response['active']).to be_falsey
- expect(integration_response['url']).to include('modified-name')
- end
+ it_behaves_like 'updating an existing HTTP integration'
end
diff --git a/spec/requests/api/graphql/mutations/boards/lists/create_spec.rb b/spec/requests/api/graphql/mutations/boards/lists/create_spec.rb
index 328f4fb7b6e..fec9a8c6307 100644
--- a/spec/requests/api/graphql/mutations/boards/lists/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/boards/lists/create_spec.rb
@@ -3,52 +3,10 @@
require 'spec_helper'
RSpec.describe 'Create a label or backlog board list' do
- include GraphqlHelpers
-
let_it_be(:group) { create(:group, :private) }
let_it_be(:board) { create(:board, group: group) }
- let_it_be(:user) { create(:user) }
- let_it_be(:dev_label) do
- create(:group_label, title: 'Development', color: '#FFAABB', group: group)
- end
-
- let(:current_user) { user }
- let(:mutation) { graphql_mutation(:board_list_create, input) }
- let(:mutation_response) { graphql_mutation_response(:board_list_create) }
-
- context 'the user is not allowed to read board lists' do
- let(:input) { { board_id: board.to_global_id.to_s, backlog: true } }
-
- it_behaves_like 'a mutation that returns a top-level access error'
- end
-
- context 'when user has permissions to admin board lists' do
- before do
- group.add_reporter(current_user)
- end
-
- describe 'backlog list' do
- let(:input) { { board_id: board.to_global_id.to_s, backlog: true } }
-
- it 'creates the list' do
- post_graphql_mutation(mutation, current_user: current_user)
-
- expect(response).to have_gitlab_http_status(:success)
- expect(mutation_response['list'])
- .to include('position' => nil, 'listType' => 'backlog')
- end
- end
-
- describe 'label list' do
- let(:input) { { board_id: board.to_global_id.to_s, label_id: dev_label.to_global_id.to_s } }
-
- it 'creates the list' do
- post_graphql_mutation(mutation, current_user: current_user)
- expect(response).to have_gitlab_http_status(:success)
- expect(mutation_response['list'])
- .to include('position' => 0, 'listType' => 'label', 'label' => include('title' => 'Development'))
- end
- end
+ it_behaves_like 'board lists create request' do
+ let(:mutation_name) { :board_list_create }
end
end
diff --git a/spec/requests/api/graphql/mutations/ci/ci_cd_settings_update_spec.rb b/spec/requests/api/graphql/mutations/ci/ci_cd_settings_update_spec.rb
index 283badeaf33..0dcae28ac5d 100644
--- a/spec/requests/api/graphql/mutations/ci/ci_cd_settings_update_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/ci_cd_settings_update_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'CiCdSettingsUpdate' do
include GraphqlHelpers
- let_it_be(:project) { create(:project, ci_keep_latest_artifact: true) }
+ let_it_be(:project) { create(:project, keep_latest_artifact: true) }
let(:variables) { { full_path: project.full_path, keep_latest_artifact: false } }
let(:mutation) { graphql_mutation(:ci_cd_settings_update, variables) }
@@ -42,7 +42,7 @@ RSpec.describe 'CiCdSettingsUpdate' do
project.reload
expect(response).to have_gitlab_http_status(:success)
- expect(project.ci_keep_latest_artifact).to eq(false)
+ expect(project.keep_latest_artifact).to eq(false)
end
context 'when bad arguments are provided' do
diff --git a/spec/requests/api/graphql/mutations/merge_requests/reviewer_rereview_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/reviewer_rereview_spec.rb
new file mode 100644
index 00000000000..2e4f35cbcde
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/merge_requests/reviewer_rereview_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Setting assignees of a merge request' do
+ include GraphqlHelpers
+
+ let(:current_user) { create(:user) }
+ let(:merge_request) { create(:merge_request, reviewers: [user]) }
+ let(:project) { merge_request.project }
+ let(:user) { create(:user) }
+ let(:input) { { user_id: global_id_of(user) } }
+
+ let(:mutation) do
+ variables = {
+ project_path: project.full_path,
+ iid: merge_request.iid.to_s
+ }
+ graphql_mutation(:merge_request_reviewer_rereview, variables.merge(input),
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ QL
+ )
+ end
+
+ def mutation_response
+ graphql_mutation_response(:merge_request_reviewer_rereview)
+ end
+
+ def mutation_errors
+ mutation_response['errors']
+ end
+
+ before do
+ project.add_developer(current_user)
+ project.add_developer(user)
+ end
+
+ it 'returns an error if the user is not allowed to update the merge request' do
+ post_graphql_mutation(mutation, current_user: create(:user))
+
+ expect(graphql_errors).not_to be_empty
+ end
+
+ describe 'reviewer does not exist' do
+ let(:input) { { user_id: global_id_of(create(:user)) } }
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_errors).not_to be_empty
+ end
+ end
+
+ describe 'reviewer exists' do
+ it 'does not return an error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_errors).to be_empty
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb b/spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb
index 21da1332465..7dd897f6466 100644
--- a/spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb
+++ b/spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb
@@ -43,6 +43,8 @@ RSpec.describe 'Adding a DiffNote' do
it_behaves_like 'a Note mutation when there are active record validation errors', model: DiffNote
+ it_behaves_like 'a Note mutation when there are rate limit validation errors'
+
context do
let(:diff_refs) { build(:commit).diff_refs } # Allow fake diff refs so arguments are valid
diff --git a/spec/requests/api/graphql/mutations/notes/create/image_diff_note_spec.rb b/spec/requests/api/graphql/mutations/notes/create/image_diff_note_spec.rb
index 8bc68e6017c..0e5744fb64f 100644
--- a/spec/requests/api/graphql/mutations/notes/create/image_diff_note_spec.rb
+++ b/spec/requests/api/graphql/mutations/notes/create/image_diff_note_spec.rb
@@ -46,6 +46,8 @@ RSpec.describe 'Adding an image DiffNote' do
it_behaves_like 'a Note mutation when there are active record validation errors', model: DiffNote
+ it_behaves_like 'a Note mutation when there are rate limit validation errors'
+
context do
let(:diff_refs) { build(:commit).diff_refs } # Allow fake diff refs so arguments are valid
diff --git a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb
index 6d761eb0a54..1eed1c8e2ae 100644
--- a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb
+++ b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb
@@ -37,6 +37,8 @@ RSpec.describe 'Adding a Note' do
it_behaves_like 'a Note mutation when the given resource id is not for a Noteable'
+ it_behaves_like 'a Note mutation when there are rate limit validation errors'
+
it 'returns the note' do
post_graphql_mutation(mutation, current_user: current_user)
diff --git a/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb b/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb
index 713b26a6a9b..1ce09881fde 100644
--- a/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb
+++ b/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'Updating an image DiffNote' do
include GraphqlHelpers
using RSpec::Parameterized::TableSyntax
- let_it_be(:noteable) { create(:merge_request, :with_diffs) }
+ let_it_be(:noteable) { create(:merge_request) }
let_it_be(:original_body) { 'Original body' }
let_it_be(:original_position) do
Gitlab::Diff::Position.new(
diff --git a/spec/requests/api/graphql/mutations/releases/create_spec.rb b/spec/requests/api/graphql/mutations/releases/create_spec.rb
index 79bdcec7944..a4918cd560c 100644
--- a/spec/requests/api/graphql/mutations/releases/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/releases/create_spec.rb
@@ -309,10 +309,7 @@ RSpec.describe 'Creation of a new release' do
let(:asset_link_2) { { name: 'My link', url: 'https://example.com/2' } }
let(:assets) { { links: [asset_link_1, asset_link_2] } }
- # Right now the raw Postgres error message is sent to the user as the validation message.
- # We should catch this validation error and return a nicer message:
- # https://gitlab.com/gitlab-org/gitlab/-/issues/277087
- it_behaves_like 'errors-as-data with message', 'PG::UniqueViolation'
+ it_behaves_like 'errors-as-data with message', %r{Validation failed: Links have duplicate values \(My link\)}
end
context 'when two release assets share the same URL' do
@@ -320,8 +317,7 @@ RSpec.describe 'Creation of a new release' do
let(:asset_link_2) { { name: 'My second link', url: 'https://example.com' } }
let(:assets) { { links: [asset_link_1, asset_link_2] } }
- # Same note as above about the ugly error message
- it_behaves_like 'errors-as-data with message', 'PG::UniqueViolation'
+ it_behaves_like 'errors-as-data with message', %r{Validation failed: Links have duplicate values \(https://example.com\)}
end
context 'when the provided tag name is HEAD' do
diff --git a/spec/requests/api/graphql/mutations/snippets/create_spec.rb b/spec/requests/api/graphql/mutations/snippets/create_spec.rb
index fd0dc98a8d3..1c2260070ec 100644
--- a/spec/requests/api/graphql/mutations/snippets/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/create_spec.rb
@@ -17,6 +17,7 @@ RSpec.describe 'Creating a Snippet' do
let(:actions) { [{ action: action }.merge(file_1), { action: action }.merge(file_2)] }
let(:project_path) { nil }
let(:uploaded_files) { nil }
+ let(:spam_mutation_vars) { {} }
let(:mutation_vars) do
{
description: description,
@@ -25,7 +26,7 @@ RSpec.describe 'Creating a Snippet' do
project_path: project_path,
uploaded_files: uploaded_files,
blob_actions: actions
- }
+ }.merge(spam_mutation_vars)
end
let(:mutation) do
@@ -77,7 +78,20 @@ RSpec.describe 'Creating a Snippet' do
expect(mutation_response['snippet']).to be_nil
end
- it_behaves_like 'spam flag is present'
+ context 'when snippet_spam flag is disabled' do
+ before do
+ stub_feature_flags(snippet_spam: false)
+ end
+
+ it 'passes disable_spam_action_service param to service' do
+ expect(::Snippets::CreateService)
+ .to receive(:new)
+ .with(anything, anything, hash_including(disable_spam_action_service: true))
+ .and_call_original
+
+ subject
+ end
+ end
end
shared_examples 'creates snippet' do
@@ -98,15 +112,24 @@ RSpec.describe 'Creating a Snippet' do
end
context 'when action is invalid' do
- let(:file_1) { { filePath: 'example_file1' }}
+ let(:file_1) { { filePath: 'example_file1' } }
it_behaves_like 'a mutation that returns errors in the response', errors: ['Snippet actions have invalid data']
it_behaves_like 'does not create snippet'
end
it_behaves_like 'snippet edit usage data counters'
- it_behaves_like 'spam flag is present'
- it_behaves_like 'can raise spam flag' do
+
+ it_behaves_like 'a mutation which can mutate a spammable' do
+ let(:captcha_response) { 'abc123' }
+ let(:spam_log_id) { 1234 }
+ let(:spam_mutation_vars) do
+ {
+ captcha_response: captcha_response,
+ spam_log_id: spam_log_id
+ }
+ end
+
let(:service) { Snippets::CreateService }
end
end
@@ -148,9 +171,6 @@ RSpec.describe 'Creating a Snippet' do
it_behaves_like 'a mutation that returns errors in the response', errors: ["Title can't be blank"]
it_behaves_like 'does not create snippet'
- it_behaves_like 'can raise spam flag' do
- let(:service) { Snippets::CreateService }
- end
end
context 'when there non ActiveRecord errors' do
diff --git a/spec/requests/api/graphql/mutations/snippets/update_spec.rb b/spec/requests/api/graphql/mutations/snippets/update_spec.rb
index 21d403c6f73..43dc8d8bc44 100644
--- a/spec/requests/api/graphql/mutations/snippets/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/update_spec.rb
@@ -16,6 +16,7 @@ RSpec.describe 'Updating a Snippet' do
let(:updated_file) { 'CHANGELOG' }
let(:deleted_file) { 'README' }
let(:snippet_gid) { GitlabSchema.id_from_object(snippet).to_s }
+ let(:spam_mutation_vars) { {} }
let(:mutation_vars) do
{
id: snippet_gid,
@@ -26,7 +27,7 @@ RSpec.describe 'Updating a Snippet' do
{ action: :update, filePath: updated_file, content: updated_content },
{ action: :delete, filePath: deleted_file }
]
- }
+ }.merge(spam_mutation_vars)
end
let(:mutation) do
@@ -81,11 +82,20 @@ RSpec.describe 'Updating a Snippet' do
end
end
- it_behaves_like 'can raise spam flag' do
- let(:service) { Snippets::UpdateService }
- end
+ context 'when snippet_spam flag is disabled' do
+ before do
+ stub_feature_flags(snippet_spam: false)
+ end
- it_behaves_like 'spam flag is present'
+ it 'passes disable_spam_action_service param to service' do
+ expect(::Snippets::UpdateService)
+ .to receive(:new)
+ .with(anything, anything, hash_including(disable_spam_action_service: true))
+ .and_call_original
+
+ subject
+ end
+ end
context 'when there are ActiveRecord validation errors' do
let(:updated_title) { '' }
@@ -112,11 +122,19 @@ RSpec.describe 'Updating a Snippet' do
expect(mutation_response['snippet']['visibilityLevel']).to eq('private')
end
end
+ end
- it_behaves_like 'spam flag is present'
- it_behaves_like 'can raise spam flag' do
- let(:service) { Snippets::UpdateService }
+ it_behaves_like 'a mutation which can mutate a spammable' do
+ let(:captcha_response) { 'abc123' }
+ let(:spam_log_id) { 1234 }
+ let(:spam_mutation_vars) do
+ {
+ captcha_response: captcha_response,
+ spam_log_id: spam_log_id
+ }
end
+
+ let(:service) { Snippets::UpdateService }
end
def blob_at(filename)
diff --git a/spec/requests/api/graphql/packages/package_composer_details_spec.rb b/spec/requests/api/graphql/packages/package_composer_details_spec.rb
deleted file mode 100644
index 1a2cf4a972a..00000000000
--- a/spec/requests/api/graphql/packages/package_composer_details_spec.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-# frozen_string_literal: true
-require 'spec_helper'
-
-RSpec.describe 'package composer details' do
- using RSpec::Parameterized::TableSyntax
- include GraphqlHelpers
-
- let_it_be(:project) { create(:project) }
- let_it_be(:package) { create(:composer_package, project: project) }
- let_it_be(:composer_metadatum) do
- # we are forced to manually create the metadatum, without using the factory to force the sha to be a string
- # and avoid an error where gitaly can't find the repository
- create(:composer_metadatum, package: package, target_sha: 'foo_sha', composer_json: { name: 'name', type: 'type', license: 'license', version: 1 })
- end
-
- let(:query) do
- graphql_query_for(
- 'packageComposerDetails',
- { id: package_global_id },
- all_graphql_fields_for('PackageComposerDetails', max_depth: 2)
- )
- end
-
- let(:user) { project.owner }
- let(:package_global_id) { package.to_global_id.to_s }
- let(:package_composer_details_response) { graphql_data.dig('packageComposerDetails') }
-
- subject { post_graphql(query, current_user: user) }
-
- it_behaves_like 'a working graphql query' do
- before do
- subject
- end
-
- it 'matches the JSON schema' do
- expect(package_composer_details_response).to match_schema('graphql/packages/package_composer_details')
- end
- end
-end
diff --git a/spec/requests/api/graphql/packages/package_spec.rb b/spec/requests/api/graphql/packages/package_spec.rb
new file mode 100644
index 00000000000..bb3ceb81f16
--- /dev/null
+++ b/spec/requests/api/graphql/packages/package_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe 'package details' do
+ using RSpec::Parameterized::TableSyntax
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:package) { create(:composer_package, project: project) }
+ let_it_be(:composer_json) { { name: 'name', type: 'type', license: 'license', version: 1 } }
+ let_it_be(:composer_metadatum) do
+ # we are forced to manually create the metadatum, without using the factory to force the sha to be a string
+ # and avoid an error where gitaly can't find the repository
+ create(:composer_metadatum, package: package, target_sha: 'foo_sha', composer_json: composer_json)
+ end
+
+ let(:depth) { 3 }
+ let(:excluded) { %w[metadata apiFuzzingCiConfiguration] }
+
+ let(:query) do
+ graphql_query_for(:package, { id: package_global_id }, <<~FIELDS)
+ #{all_graphql_fields_for('Package', max_depth: depth, excluded: excluded)}
+ metadata {
+ #{query_graphql_fragment('ComposerMetadata')}
+ }
+ FIELDS
+ end
+
+ let(:user) { project.owner }
+ let(:package_global_id) { global_id_of(package) }
+ let(:package_details) { graphql_data_at(:package) }
+
+ subject { post_graphql(query, current_user: user) }
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ subject
+ end
+
+ it 'matches the JSON schema' do
+ expect(package_details).to match_schema('graphql/packages/package_details')
+ end
+
+ it 'includes the fields of the correct package' do
+ expect(package_details).to include(
+ 'id' => package_global_id,
+ 'metadata' => {
+ 'targetSha' => 'foo_sha',
+ 'composerJson' => composer_json.transform_keys(&:to_s).transform_values(&:to_s)
+ }
+ )
+ end
+ end
+
+ context 'there are other versions of this package' do
+ let(:depth) { 3 }
+ let(:excluded) { %w[metadata project tags pipelines] } # to limit the query complexity
+
+ let_it_be(:siblings) { create_list(:composer_package, 2, project: project, name: package.name) }
+
+ it 'includes the sibling versions' do
+ subject
+
+ expect(graphql_data_at(:package, :versions, :nodes)).to match_array(
+ siblings.map { |p| a_hash_including('id' => global_id_of(p)) }
+ )
+ end
+
+ context 'going deeper' do
+ let(:depth) { 6 }
+
+ it 'does not create a cycle of versions' do
+ subject
+
+ expect(graphql_data_at(:package, :versions, :nodes, :version)).to be_present
+ expect(graphql_data_at(:package, :versions, :nodes, :versions)).not_to be_present
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/container_repositories_spec.rb b/spec/requests/api/graphql/project/container_repositories_spec.rb
index 6b1c8689515..2087d8c2cc3 100644
--- a/spec/requests/api/graphql/project/container_repositories_spec.rb
+++ b/spec/requests/api/graphql/project/container_repositories_spec.rb
@@ -156,4 +156,51 @@ RSpec.describe 'getting container repositories in a project' do
expect(container_repositories_count_response).to eq(container_repositories.size)
end
+
+ describe 'sorting and pagination' do
+ let_it_be(:data_path) { [:project, :container_repositories] }
+ let_it_be(:sort_project) { create(:project, :public) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:container_repository1) { create(:container_repository, name: 'b', project: sort_project) }
+ let_it_be(:container_repository2) { create(:container_repository, name: 'a', project: sort_project) }
+ let_it_be(:container_repository3) { create(:container_repository, name: 'd', project: sort_project) }
+ let_it_be(:container_repository4) { create(:container_repository, name: 'c', project: sort_project) }
+ let_it_be(:container_repository5) { create(:container_repository, name: 'e', project: sort_project) }
+
+ before do
+ stub_container_registry_tags(repository: container_repository1.path, tags: %w(tag1 tag1 tag3), with_manifest: false)
+ stub_container_registry_tags(repository: container_repository2.path, tags: %w(tag4 tag5 tag6), with_manifest: false)
+ stub_container_registry_tags(repository: container_repository3.path, tags: %w(tag7 tag8), with_manifest: false)
+ stub_container_registry_tags(repository: container_repository4.path, tags: %w(tag9), with_manifest: false)
+ stub_container_registry_tags(repository: container_repository5.path, tags: %w(tag10 tag11), with_manifest: false)
+ end
+
+ def pagination_query(params)
+ graphql_query_for(:project, { full_path: sort_project.full_path },
+ query_nodes(:container_repositories, :name, include_pagination_info: true, args: params)
+ )
+ end
+
+ def pagination_results_data(data)
+ data.map { |container_repository| container_repository.dig('name') }
+ end
+
+ context 'when sorting by name' do
+ context 'when ascending' do
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { :NAME_ASC }
+ let(:first_param) { 2 }
+ let(:expected_results) { [container_repository2.name, container_repository1.name, container_repository4.name, container_repository3.name, container_repository5.name] }
+ end
+ end
+
+ context 'when descending' do
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { :NAME_DESC }
+ let(:first_param) { 2 }
+ let(:expected_results) { [container_repository5.name, container_repository3.name, container_repository4.name, container_repository1.name, container_repository2.name] }
+ end
+ end
+ end
+ end
end
diff --git a/spec/requests/api/graphql/project/issue/designs/notes_spec.rb b/spec/requests/api/graphql/project/issue/designs/notes_spec.rb
index a671ddc7ab1..7148750b6cb 100644
--- a/spec/requests/api/graphql/project/issue/designs/notes_spec.rb
+++ b/spec/requests/api/graphql/project/issue/designs/notes_spec.rb
@@ -22,6 +22,23 @@ RSpec.describe 'Getting designs related to an issue' do
end
end
+ it_behaves_like 'a noteable graphql type we can query' do
+ let(:noteable) { design }
+ let(:note_factory) { :diff_note_on_design }
+ let(:discussion_factory) { :diff_note_on_design }
+ let(:path_to_noteable) { [:issue, :design_collection, :designs, :nodes, 0] }
+
+ before do
+ project.add_developer(current_user)
+ end
+
+ def query(fields)
+ graphql_query_for(:issue, { id: global_id_of(issue) }, <<~FIELDS)
+ designCollection { designs { nodes { #{fields} } } }
+ FIELDS
+ end
+ end
+
it 'is not too deep for anonymous users' do
note_fields = <<~FIELDS
id
@@ -37,7 +54,7 @@ RSpec.describe 'Getting designs related to an issue' do
expect(note_data['id']).to eq(note.to_global_id.to_s)
end
- def query(note_fields = all_graphql_fields_for(Note))
+ def query(note_fields = all_graphql_fields_for(Note, max_depth: 1))
design_node = <<~NODE
designs {
nodes {
diff --git a/spec/requests/api/graphql/project/merge_request/pipelines_spec.rb b/spec/requests/api/graphql/project/merge_request/pipelines_spec.rb
index ac0b18a37d6..70c5bda35e1 100644
--- a/spec/requests/api/graphql/project/merge_request/pipelines_spec.rb
+++ b/spec/requests/api/graphql/project/merge_request/pipelines_spec.rb
@@ -8,9 +8,11 @@ RSpec.describe 'Query.project.mergeRequests.pipelines' do
let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:author) { create(:user) }
let_it_be(:merge_requests) do
- %i[with_diffs with_image_diffs conflict].map do |trait|
- create(:merge_request, trait, author: author, source_project: project)
- end
+ [
+ create(:merge_request, author: author, source_project: project),
+ create(:merge_request, :with_image_diffs, author: author, source_project: project),
+ create(:merge_request, :conflict, author: author, source_project: project)
+ ]
end
describe '.count' do
diff --git a/spec/requests/api/graphql/project/merge_request_spec.rb b/spec/requests/api/graphql/project/merge_request_spec.rb
index e1b867ad097..a4e8d0bc35e 100644
--- a/spec/requests/api/graphql/project/merge_request_spec.rb
+++ b/spec/requests/api/graphql/project/merge_request_spec.rb
@@ -66,14 +66,6 @@ RSpec.describe 'getting merge request information nested in a project' do
expect(graphql_data_at(:project, :merge_request, :reviewers, :nodes)).to match_array(expected)
expect(graphql_data_at(:project, :merge_request, :participants, :nodes)).to include(*expected)
end
-
- it 'suppresses reviewers if reviewers are not allowed' do
- stub_feature_flags(merge_request_reviewers: false)
-
- post_graphql(query, current_user: current_user)
-
- expect(graphql_data_at(:project, :merge_request, :reviewers)).to be_nil
- end
end
it 'includes diff stats' do
diff --git a/spec/requests/api/graphql/project/merge_requests_spec.rb b/spec/requests/api/graphql/project/merge_requests_spec.rb
index c85cb8b2ffe..d684be91dc9 100644
--- a/spec/requests/api/graphql/project/merge_requests_spec.rb
+++ b/spec/requests/api/graphql/project/merge_requests_spec.rb
@@ -196,17 +196,18 @@ RSpec.describe 'getting merge request listings nested in a project' do
end
context 'when requesting `commit_count`' do
- let(:requested_fields) { [:commit_count] }
+ let(:merge_request_with_commits) { create(:merge_request, source_project: project) }
+ let(:search_params) { { iids: [merge_request_a.iid.to_s, merge_request_with_commits.iid.to_s] } }
+ let(:requested_fields) { [:iid, :commit_count] }
it 'exposes `commit_count`' do
- merge_request_a.metrics.update!(commits_count: 5)
-
execute_query
- expect(results).to include(a_hash_including('commitCount' => 5))
+ expect(results).to match_array([
+ { "iid" => merge_request_a.iid.to_s, "commitCount" => 0 },
+ { "iid" => merge_request_with_commits.iid.to_s, "commitCount" => 29 }
+ ])
end
-
- include_examples 'N+1 query check'
end
context 'when requesting `merged_at`' do
@@ -264,18 +265,6 @@ RSpec.describe 'getting merge request listings nested in a project' do
})
end
- context 'the feature flag is disabled' do
- before do
- stub_feature_flags(merge_request_reviewers: false)
- end
-
- it 'does not return reviewers' do
- execute_query
-
- expect(results).to all(match a_hash_including('reviewers' => be_nil))
- end
- end
-
include_examples 'N+1 query check'
end
end
@@ -396,4 +385,87 @@ RSpec.describe 'getting merge request listings nested in a project' do
end
end
end
+
+ context 'when only the count is requested' do
+ context 'when merged at filter is present' do
+ let_it_be(:merge_request) do
+ create(:merge_request, :unique_branches, source_project: project).tap do |mr|
+ mr.metrics.update!(merged_at: Time.new(2020, 1, 3))
+ end
+ end
+
+ let(:query) do
+ # Note: __typename meta field is always requested by the FE
+ graphql_query_for(:project, { full_path: project.full_path },
+ <<~QUERY
+ mergeRequests(mergedAfter: "2020-01-01", mergedBefore: "2020-01-05", first: 0, sourceBranches: null, labels: null) {
+ count
+ __typename
+ }
+ QUERY
+ )
+ end
+
+ shared_examples 'count examples' do
+ it 'returns the correct count' do
+ post_graphql(query, current_user: current_user)
+
+ count = graphql_data.dig('project', 'mergeRequests', 'count')
+ expect(count).to eq(1)
+ end
+ end
+
+ context 'when "optimized_merge_request_count_with_merged_at_filter" feature flag is enabled' do
+ before do
+ stub_feature_flags(optimized_merge_request_count_with_merged_at_filter: true)
+ end
+
+ it 'does not query the merge requests table for the count' do
+ query_recorder = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: current_user) }
+
+ queries = query_recorder.data.each_value.first[:occurrences]
+ expect(queries).not_to include(match(/SELECT COUNT\(\*\) FROM "merge_requests"/))
+ expect(queries).to include(match(/SELECT COUNT\(\*\) FROM "merge_request_metrics"/))
+ end
+
+ context 'when total_time_to_merge and count is queried' do
+ let(:query) do
+ graphql_query_for(:project, { full_path: project.full_path },
+ <<~QUERY
+ mergeRequests(mergedAfter: "2020-01-01", mergedBefore: "2020-01-05", first: 0) {
+ totalTimeToMerge
+ count
+ }
+ QUERY
+ )
+ end
+
+ it 'does not query the merge requests table for the total_time_to_merge' do
+ query_recorder = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: current_user) }
+
+ queries = query_recorder.data.each_value.first[:occurrences]
+ expect(queries).to include(match(/SELECT.+SUM.+FROM "merge_request_metrics" WHERE/))
+ end
+ end
+
+ it_behaves_like 'count examples'
+
+ context 'when "optimized_merge_request_count_with_merged_at_filter" feature flag is disabled' do
+ before do
+ stub_feature_flags(optimized_merge_request_count_with_merged_at_filter: false)
+ end
+
+ it 'queries the merge requests table for the count' do
+ query_recorder = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: current_user) }
+
+ queries = query_recorder.data.each_value.first[:occurrences]
+ expect(queries).to include(match(/SELECT COUNT\(\*\) FROM "merge_requests"/))
+ expect(queries).not_to include(match(/SELECT COUNT\(\*\) FROM "merge_request_metrics"/))
+ end
+
+ it_behaves_like 'count examples'
+ end
+ end
+ end
+ end
end
diff --git a/spec/requests/api/graphql/project/packages_spec.rb b/spec/requests/api/graphql/project/packages_spec.rb
index 5df98ed1e6b..b20c96d54c8 100644
--- a/spec/requests/api/graphql/project/packages_spec.rb
+++ b/spec/requests/api/graphql/project/packages_spec.rb
@@ -5,16 +5,27 @@ require 'spec_helper'
RSpec.describe 'getting a package list for a project' do
include GraphqlHelpers
- let_it_be(:project) { create(:project) }
+ let_it_be(:project) { create(:project, :repository) }
let_it_be(:current_user) { create(:user) }
+
let_it_be(:package) { create(:package, project: project) }
- let(:packages_data) { graphql_data['project']['packages']['edges'] }
+ let_it_be(:maven_package) { create(:maven_package, project: project) }
+ let_it_be(:debian_package) { create(:debian_package, project: project) }
+ let_it_be(:composer_package) { create(:composer_package, project: project) }
+ let_it_be(:composer_metadatum) do
+ create(:composer_metadatum, package: composer_package,
+ target_sha: 'afdeh',
+ composer_json: { name: 'x', type: 'y', license: 'z', version: 1 })
+ end
+
+ let(:package_names) { graphql_data_at(:project, :packages, :edges, :node, :name) }
let(:fields) do
<<~QUERY
edges {
node {
- #{all_graphql_fields_for('packages'.classify)}
+ #{all_graphql_fields_for('packages'.classify, excluded: ['project'])}
+ metadata { #{query_graphql_fragment('ComposerMetadata')} }
}
}
QUERY
@@ -37,7 +48,17 @@ RSpec.describe 'getting a package list for a project' do
it_behaves_like 'a working graphql query'
it 'returns packages successfully' do
- expect(packages_data[0]['node']['name']).to eq package.name
+ expect(package_names).to contain_exactly(
+ package.name,
+ maven_package.name,
+ debian_package.name,
+ composer_package.name
+ )
+ end
+
+ it 'deals with metadata' do
+ target_shas = graphql_data_at(:project, :packages, :edges, :node, :metadata, :target_sha)
+ expect(target_shas).to contain_exactly(composer_metadatum.target_sha)
end
end
@@ -53,7 +74,7 @@ RSpec.describe 'getting a package list for a project' do
end
end
- context 'when the user is not autenthicated' do
+ context 'when the user is not authenticated' do
before do
post_graphql(query)
end
diff --git a/spec/requests/api/graphql/project/release_spec.rb b/spec/requests/api/graphql/project/release_spec.rb
index ccc2825da25..72197f00df4 100644
--- a/spec/requests/api/graphql/project/release_spec.rb
+++ b/spec/requests/api/graphql/project/release_spec.rb
@@ -283,7 +283,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
let_it_be(:project) { create(:project, :repository, :private) }
let_it_be(:milestone_1) { create(:milestone, project: project) }
let_it_be(:milestone_2) { create(:milestone, project: project) }
- let_it_be(:release) { create(:release, :with_evidence, project: project, milestones: [milestone_1, milestone_2], released_at: released_at) }
+ let_it_be(:release, reload: true) { create(:release, :with_evidence, project: project, milestones: [milestone_1, milestone_2], released_at: released_at) }
let_it_be(:release_link_1) { create(:release_link, release: release) }
let_it_be(:release_link_2) { create(:release_link, release: release, filepath: link_filepath) }
@@ -324,7 +324,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:milestone_1) { create(:milestone, project: project) }
let_it_be(:milestone_2) { create(:milestone, project: project) }
- let_it_be(:release) { create(:release, :with_evidence, project: project, milestones: [milestone_1, milestone_2], released_at: released_at) }
+ let_it_be(:release, reload: true) { create(:release, :with_evidence, project: project, milestones: [milestone_1, milestone_2], released_at: released_at) }
let_it_be(:release_link_1) { create(:release_link, release: release) }
let_it_be(:release_link_2) { create(:release_link, release: release, filepath: link_filepath) }
@@ -435,13 +435,13 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
let_it_be_with_reload(:release) { create(:release, project: project) }
let(:release_fields) do
- query_graphql_field(%{
+ %{
milestones {
nodes {
title
}
}
- })
+ }
end
let(:actual_milestone_title_order) do
diff --git a/spec/requests/api/graphql/project/terraform/state_spec.rb b/spec/requests/api/graphql/project/terraform/state_spec.rb
new file mode 100644
index 00000000000..9f1d9ab204a
--- /dev/null
+++ b/spec/requests/api/graphql/project/terraform/state_spec.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'query a single terraform state' do
+ include GraphqlHelpers
+ include ::API::Helpers::RelatedResourcesHelpers
+
+ let_it_be(:terraform_state) { create(:terraform_state, :with_version, :locked) }
+
+ let(:latest_version) { terraform_state.latest_version }
+ let(:project) { terraform_state.project }
+ let(:current_user) { project.creator }
+ let(:data) { graphql_data.dig('project', 'terraformState') }
+
+ let(:query) do
+ graphql_query_for(
+ :project,
+ { fullPath: project.full_path },
+ query_graphql_field(
+ :terraformState,
+ { name: terraform_state.name },
+ %{
+ id
+ name
+ lockedAt
+ createdAt
+ updatedAt
+
+ latestVersion {
+ id
+ serial
+ createdAt
+ updatedAt
+
+ createdByUser {
+ id
+ }
+
+ job {
+ name
+ }
+ }
+
+ lockedByUser {
+ id
+ }
+ }
+ )
+ )
+ end
+
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns terraform state data' do
+ expect(data).to match(a_hash_including({
+ 'id' => global_id_of(terraform_state),
+ 'name' => terraform_state.name,
+ 'lockedAt' => terraform_state.locked_at.iso8601,
+ 'createdAt' => terraform_state.created_at.iso8601,
+ 'updatedAt' => terraform_state.updated_at.iso8601,
+ 'lockedByUser' => { 'id' => global_id_of(terraform_state.locked_by_user) },
+ 'latestVersion' => {
+ 'id' => eq(global_id_of(latest_version)),
+ 'serial' => eq(latest_version.version),
+ 'createdAt' => eq(latest_version.created_at.iso8601),
+ 'updatedAt' => eq(latest_version.updated_at.iso8601),
+ 'createdByUser' => { 'id' => eq(global_id_of(latest_version.created_by_user)) },
+ 'job' => { 'name' => eq(latest_version.build.name) }
+ }
+ }))
+ end
+
+ context 'unauthorized users' do
+ let(:current_user) { nil }
+
+ it { expect(data).to be_nil }
+ end
+end
diff --git a/spec/requests/api/group_import_spec.rb b/spec/requests/api/group_import_spec.rb
index d8e945baf6a..bb7436502ed 100644
--- a/spec/requests/api/group_import_spec.rb
+++ b/spec/requests/api/group_import_spec.rb
@@ -5,13 +5,13 @@ require 'spec_helper'
RSpec.describe API::GroupImport do
include WorkhorseHelpers
+ include_context 'workhorse headers'
+
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let(:path) { '/groups/import' }
let(:file) { File.join('spec', 'fixtures', 'group_export.tar.gz') }
let(:export_path) { "#{Dir.tmpdir}/group_export_spec" }
- let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
- let(:workhorse_headers) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
before do
allow_next_instance_of(Gitlab::ImportExport) do |import_export|
diff --git a/spec/requests/api/group_labels_spec.rb b/spec/requests/api/group_labels_spec.rb
index 72621e2ce5e..c677e68b285 100644
--- a/spec/requests/api/group_labels_spec.rb
+++ b/spec/requests/api/group_labels_spec.rb
@@ -3,13 +3,19 @@
require 'spec_helper'
RSpec.describe API::GroupLabels do
+ let_it_be(:valid_group_label_title_1) { 'Label foo & bar:subgroup::v.1' }
+ let_it_be(:valid_group_label_title_1_esc) { ERB::Util.url_encode(valid_group_label_title_1) }
+ let_it_be(:valid_group_label_title_2) { 'Bar & foo:subgroup::v.2' }
+ let_it_be(:valid_subgroup_label_title_1) { 'Support label foobar:sub::v.1' }
+ let_it_be(:valid_new_label_title) { 'New & foo:feature::v.3' }
+
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:subgroup) { create(:group, parent: group) }
let!(:group_member) { create(:group_member, group: group, user: user) }
- let!(:group_label1) { create(:group_label, title: 'feature-label', group: group) }
- let!(:group_label2) { create(:group_label, title: 'bug', group: group) }
- let!(:subgroup_label) { create(:group_label, title: 'support-label', group: subgroup) }
+ let!(:group_label1) { create(:group_label, title: valid_group_label_title_1, group: group) }
+ let!(:group_label2) { create(:group_label, title: valid_group_label_title_2, group: group) }
+ let!(:subgroup_label) { create(:group_label, title: valid_subgroup_label_title_1, group: subgroup) }
describe 'GET :id/labels' do
context 'get current group labels' do
@@ -104,7 +110,7 @@ RSpec.describe API::GroupLabels do
describe 'GET :id/labels/:label_id' do
it 'returns a single label for the group' do
- get api("/groups/#{group.id}/labels/#{group_label1.name}", user)
+ get api("/groups/#{group.id}/labels/#{valid_group_label_title_1_esc}", user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['name']).to eq(group_label1.name)
@@ -117,13 +123,13 @@ RSpec.describe API::GroupLabels do
it 'returns created label when all params are given' do
post api("/groups/#{group.id}/labels", user),
params: {
- name: 'Foo',
+ name: valid_new_label_title,
color: '#FFAABB',
description: 'test'
}
expect(response).to have_gitlab_http_status(:created)
- expect(json_response['name']).to eq('Foo')
+ expect(json_response['name']).to eq(valid_new_label_title)
expect(json_response['color']).to eq('#FFAABB')
expect(json_response['description']).to eq('test')
end
@@ -131,12 +137,12 @@ RSpec.describe API::GroupLabels do
it 'returns created label when only required params are given' do
post api("/groups/#{group.id}/labels", user),
params: {
- name: 'Foo & Bar',
+ name: valid_new_label_title,
color: '#FFAABB'
}
expect(response).to have_gitlab_http_status(:created)
- expect(json_response['name']).to eq('Foo & Bar')
+ expect(json_response['name']).to eq(valid_new_label_title)
expect(json_response['color']).to eq('#FFAABB')
expect(json_response['description']).to be_nil
end
@@ -204,7 +210,7 @@ RSpec.describe API::GroupLabels do
describe 'DELETE /groups/:id/labels/:label_id' do
it 'returns 204 for existing label' do
- delete api("/groups/#{group.id}/labels/#{group_label1.name}", user)
+ delete api("/groups/#{group.id}/labels/#{valid_group_label_title_1_esc}", user)
expect(response).to have_gitlab_http_status(:no_content)
end
@@ -228,7 +234,7 @@ RSpec.describe API::GroupLabels do
end
it_behaves_like '412 response' do
- let(:request) { api("/groups/#{group.id}/labels/#{group_label1.name}", user) }
+ let(:request) { api("/groups/#{group.id}/labels/#{valid_group_label_title_1_esc}", user) }
end
end
@@ -237,13 +243,13 @@ RSpec.describe API::GroupLabels do
put api("/groups/#{group.id}/labels", user),
params: {
name: group_label1.name,
- new_name: 'New Label',
+ new_name: valid_new_label_title,
color: '#FFFFFF',
description: 'test'
}
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['name']).to eq('New Label')
+ expect(json_response['name']).to eq(valid_new_label_title)
expect(json_response['color']).to eq('#FFFFFF')
expect(json_response['description']).to eq('test')
end
@@ -255,11 +261,11 @@ RSpec.describe API::GroupLabels do
put api("/groups/#{subgroup.id}/labels", user),
params: {
name: subgroup_label.name,
- new_name: 'New Label'
+ new_name: valid_new_label_title
}
expect(response).to have_gitlab_http_status(:ok)
- expect(subgroup.labels[0].name).to eq('New Label')
+ expect(subgroup.labels[0].name).to eq(valid_new_label_title)
expect(group_label1.name).to eq(group_label1.title)
end
@@ -267,7 +273,7 @@ RSpec.describe API::GroupLabels do
put api("/groups/#{group.id}/labels", user),
params: {
name: 'not_exists',
- new_name: 'label3'
+ new_name: valid_new_label_title
}
expect(response).to have_gitlab_http_status(:not_found)
@@ -291,15 +297,15 @@ RSpec.describe API::GroupLabels do
describe 'PUT /groups/:id/labels/:label_id' do
it 'returns 200 if name and colors and description are changed' do
- put api("/groups/#{group.id}/labels/#{group_label1.name}", user),
+ put api("/groups/#{group.id}/labels/#{valid_group_label_title_1_esc}", user),
params: {
- new_name: 'New Label',
+ new_name: valid_new_label_title,
color: '#FFFFFF',
description: 'test'
}
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['name']).to eq('New Label')
+ expect(json_response['name']).to eq(valid_new_label_title)
expect(json_response['color']).to eq('#FFFFFF')
expect(json_response['description']).to eq('test')
end
@@ -310,25 +316,25 @@ RSpec.describe API::GroupLabels do
put api("/groups/#{subgroup.id}/labels/#{subgroup_label.name}", user),
params: {
- new_name: 'New Label'
+ new_name: valid_new_label_title
}
expect(response).to have_gitlab_http_status(:ok)
- expect(subgroup.labels[0].name).to eq('New Label')
+ expect(subgroup.labels[0].name).to eq(valid_new_label_title)
expect(group_label1.name).to eq(group_label1.title)
end
it 'returns 404 if label does not exist' do
put api("/groups/#{group.id}/labels/not_exists", user),
params: {
- new_name: 'label3'
+ new_name: valid_new_label_title
}
expect(response).to have_gitlab_http_status(:not_found)
end
it 'returns 400 if no new parameters given' do
- put api("/groups/#{group.id}/labels/#{group_label1.name}", user)
+ put api("/groups/#{group.id}/labels/#{valid_group_label_title_1_esc}", user)
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('new_name, color, description are missing, '\
@@ -339,7 +345,7 @@ RSpec.describe API::GroupLabels do
describe 'POST /groups/:id/labels/:label_id/subscribe' do
context 'when label_id is a label title' do
it 'subscribes to the label' do
- post api("/groups/#{group.id}/labels/#{group_label1.title}/subscribe", user)
+ post api("/groups/#{group.id}/labels/#{valid_group_label_title_1_esc}/subscribe", user)
expect(response).to have_gitlab_http_status(:created)
expect(json_response['name']).to eq(group_label1.title)
@@ -385,7 +391,7 @@ RSpec.describe API::GroupLabels do
context 'when label_id is a label title' do
it 'unsubscribes from the label' do
- post api("/groups/#{group.id}/labels/#{group_label1.title}/unsubscribe", user)
+ post api("/groups/#{group.id}/labels/#{valid_group_label_title_1_esc}/unsubscribe", user)
expect(response).to have_gitlab_http_status(:created)
expect(json_response['name']).to eq(group_label1.title)
diff --git a/spec/requests/api/group_packages_spec.rb b/spec/requests/api/group_packages_spec.rb
index 26895e473de..792aa2c1f20 100644
--- a/spec/requests/api/group_packages_spec.rb
+++ b/spec/requests/api/group_packages_spec.rb
@@ -144,6 +144,7 @@ RSpec.describe API::GroupPackages do
end
it_behaves_like 'with versionless packages'
+ it_behaves_like 'with status param'
it_behaves_like 'does not cause n^2 queries'
end
end
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index c7756a4fae5..1c359b6e50f 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -54,7 +54,7 @@ RSpec.describe API::Groups do
it_behaves_like 'invalid file upload request'
end
- context 'when file format is not supported' do
+ context 'when file is too large' do
let(:file_path) { 'spec/fixtures/big-image.png' }
let(:message) { 'is too big' }
@@ -661,6 +661,7 @@ RSpec.describe API::Groups do
describe 'PUT /groups/:id' do
let(:new_group_name) { 'New Group'}
+ let(:file_path) { 'spec/fixtures/dk.png' }
it_behaves_like 'group avatar upload' do
def make_upload_request
@@ -678,7 +679,8 @@ RSpec.describe API::Groups do
request_access_enabled: true,
project_creation_level: "noone",
subgroup_creation_level: "maintainer",
- default_branch_protection: ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS
+ default_branch_protection: ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS,
+ avatar: fixture_file_upload(file_path)
}
expect(response).to have_gitlab_http_status(:ok)
@@ -701,6 +703,7 @@ RSpec.describe API::Groups do
expect(json_response['shared_projects']).to be_an Array
expect(json_response['shared_projects'].length).to eq(0)
expect(json_response['default_branch_protection']).to eq(::Gitlab::Access::MAINTAINER_PROJECT_ACCESS)
+ expect(json_response['avatar_url']).to end_with('dk.png')
end
context 'updating the `default_branch_protection` attribute' do
diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb
index e04f63befd0..86999c4adaa 100644
--- a/spec/requests/api/internal/base_spec.rb
+++ b/spec/requests/api/internal/base_spec.rb
@@ -50,41 +50,6 @@ RSpec.describe API::Internal::Base do
end
end
- shared_examples 'actor key validations' do
- context 'key id is not provided' do
- let(:key_id) { nil }
-
- it 'returns an error message' do
- subject
-
- expect(json_response['success']).to be_falsey
- expect(json_response['message']).to eq('Could not find a user without a key')
- end
- end
-
- context 'key does not exist' do
- let(:key_id) { non_existing_record_id }
-
- it 'returns an error message' do
- subject
-
- expect(json_response['success']).to be_falsey
- expect(json_response['message']).to eq('Could not find the given key')
- end
- end
-
- context 'key without user' do
- let(:key_id) { create(:key, user: nil).id }
-
- it 'returns an error message' do
- subject
-
- expect(json_response['success']).to be_falsey
- expect(json_response['message']).to eq('Could not find a user for the given key')
- end
- end
- end
-
describe 'GET /internal/two_factor_recovery_codes' do
let(:key_id) { key.id }
@@ -578,25 +543,51 @@ RSpec.describe API::Internal::Base do
end
context "git pull" do
- before do
- stub_feature_flags(gitaly_mep_mep: true)
+ context "with a feature flag enabled globally" do
+ before do
+ stub_feature_flags(gitaly_mep_mep: true)
+ end
+
+ it "has the correct payload" do
+ pull(key, project)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response["status"]).to be_truthy
+ expect(json_response["gl_repository"]).to eq("project-#{project.id}")
+ expect(json_response["gl_project_path"]).to eq(project.full_path)
+ expect(json_response["gitaly"]).not_to be_nil
+ expect(json_response["gitaly"]["repository"]).not_to be_nil
+ expect(json_response["gitaly"]["repository"]["storage_name"]).to eq(project.repository.gitaly_repository.storage_name)
+ expect(json_response["gitaly"]["repository"]["relative_path"]).to eq(project.repository.gitaly_repository.relative_path)
+ expect(json_response["gitaly"]["address"]).to eq(Gitlab::GitalyClient.address(project.repository_storage))
+ expect(json_response["gitaly"]["token"]).to eq(Gitlab::GitalyClient.token(project.repository_storage))
+ expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-mep-mep' => 'true')
+ expect(user.reload.last_activity_on).to eql(Date.today)
+ end
end
- it "has the correct payload" do
- pull(key, project)
+ context "with a feature flag enabled for a project" do
+ before do
+ stub_feature_flags(gitaly_mep_mep: project)
+ end
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response["status"]).to be_truthy
- expect(json_response["gl_repository"]).to eq("project-#{project.id}")
- expect(json_response["gl_project_path"]).to eq(project.full_path)
- expect(json_response["gitaly"]).not_to be_nil
- expect(json_response["gitaly"]["repository"]).not_to be_nil
- expect(json_response["gitaly"]["repository"]["storage_name"]).to eq(project.repository.gitaly_repository.storage_name)
- expect(json_response["gitaly"]["repository"]["relative_path"]).to eq(project.repository.gitaly_repository.relative_path)
- expect(json_response["gitaly"]["address"]).to eq(Gitlab::GitalyClient.address(project.repository_storage))
- expect(json_response["gitaly"]["token"]).to eq(Gitlab::GitalyClient.token(project.repository_storage))
- expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-mep-mep' => 'true')
- expect(user.reload.last_activity_on).to eql(Date.today)
+ it "has the flag set to true for that project" do
+ pull(key, project)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response["gl_repository"]).to eq("project-#{project.id}")
+ expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-mep-mep' => 'true')
+ end
+
+ it "has the flag set to false for other projects" do
+ other_project = create(:project, :public, :repository)
+
+ pull(key, other_project)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response["gl_repository"]).to eq("project-#{other_project.id}")
+ expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-mep-mep' => 'false')
+ end
end
end
@@ -1094,6 +1085,104 @@ RSpec.describe API::Internal::Base do
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
+
+ context 'admin mode' do
+ shared_examples 'pushes succeed for ssh and http' do
+ it 'accepts the SSH push' do
+ push(key, project)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'accepts the HTTP push' do
+ push(key, project, 'http')
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ shared_examples 'pushes fail for ssh and http' do
+ it 'rejects the SSH push' do
+ push(key, project)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it 'rejects the HTTP push' do
+ push(key, project, 'http')
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'feature flag :user_mode_in_session is enabled' do
+ context 'with an admin user' do
+ let(:user) { create(:admin) }
+
+ context 'is member of the project' do
+ before do
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'pushes succeed for ssh and http'
+ end
+
+ context 'is not member of the project' do
+ it_behaves_like 'pushes succeed for ssh and http'
+ end
+ end
+
+ context 'with a regular user' do
+ context 'is member of the project' do
+ before do
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'pushes succeed for ssh and http'
+ end
+
+ context 'is not member of the project' do
+ it_behaves_like 'pushes fail for ssh and http'
+ end
+ end
+ end
+
+ context 'feature flag :user_mode_in_session is disabled' do
+ before do
+ stub_feature_flags(user_mode_in_session: false)
+ end
+
+ context 'with an admin user' do
+ let(:user) { create(:admin) }
+
+ context 'is member of the project' do
+ before do
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'pushes succeed for ssh and http'
+ end
+
+ context 'is not member of the project' do
+ it_behaves_like 'pushes succeed for ssh and http'
+ end
+ end
+
+ context 'with a regular user' do
+ context 'is member of the project' do
+ before do
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'pushes succeed for ssh and http'
+ end
+
+ context 'is not member of the project' do
+ it_behaves_like 'pushes fail for ssh and http'
+ end
+ end
+ end
+ end
end
describe 'POST /internal/post_receive', :clean_gitlab_redis_shared_state do
@@ -1308,10 +1397,6 @@ RSpec.describe API::Internal::Base do
let(:key_id) { key.id }
let(:otp) { '123456'}
- before do
- stub_feature_flags(two_factor_for_cli: true)
- end
-
subject do
post api('/internal/two_factor_otp_check'),
params: {
@@ -1321,76 +1406,10 @@ RSpec.describe API::Internal::Base do
}
end
- it_behaves_like 'actor key validations'
-
- context 'when the key is a deploy key' do
- let(:key_id) { create(:deploy_key).id }
-
- it 'returns an error message' do
- subject
-
- expect(json_response['success']).to be_falsey
- expect(json_response['message']).to eq('Deploy keys cannot be used for Two Factor')
- end
- end
-
- context 'when the two factor is enabled' do
- before do
- allow_any_instance_of(User).to receive(:two_factor_enabled?).and_return(true)
- end
-
- context 'when the OTP is valid' do
- it 'registers a new OTP session and returns success' do
- allow_any_instance_of(Users::ValidateOtpService).to receive(:execute).with(otp).and_return(status: :success)
-
- expect_next_instance_of(::Gitlab::Auth::Otp::SessionEnforcer) do |session_enforcer|
- expect(session_enforcer).to receive(:update_session).once
- end
-
- subject
-
- expect(json_response['success']).to be_truthy
- end
- end
-
- context 'when the OTP is invalid' do
- it 'is not success' do
- allow_any_instance_of(Users::ValidateOtpService).to receive(:execute).with(otp).and_return(status: :error)
-
- subject
-
- expect(json_response['success']).to be_falsey
- end
- end
- end
-
- context 'when the two factor is disabled' do
- before do
- allow_any_instance_of(User).to receive(:two_factor_enabled?).and_return(false)
- end
+ it 'is not available' do
+ subject
- it 'returns an error message' do
- subject
-
- expect(json_response['success']).to be_falsey
- expect(json_response['message']).to eq 'Two-factor authentication is not enabled for this user'
- end
- end
-
- context 'two_factor_for_cli feature is disabled' do
- before do
- stub_feature_flags(two_factor_for_cli: false)
- end
-
- context 'when two-factor is enabled for the user' do
- it 'returns user two factor config' do
- allow_any_instance_of(User).to receive(:two_factor_enabled?).and_return(true)
-
- subject
-
- expect(json_response['success']).to be_falsey
- end
- end
+ expect(json_response['success']).to be_falsey
end
end
diff --git a/spec/requests/api/internal/kubernetes_spec.rb b/spec/requests/api/internal/kubernetes_spec.rb
index afff3647b91..2e13016a0a6 100644
--- a/spec/requests/api/internal/kubernetes_spec.rb
+++ b/spec/requests/api/internal/kubernetes_spec.rb
@@ -62,25 +62,25 @@ RSpec.describe API::Internal::Kubernetes do
let!(:agent_token) { create(:cluster_agent_token) }
it 'returns no_content for valid gitops_sync_count' do
- send_request(params: { gitops_sync_count: 10 }, headers: { 'Authorization' => "Bearer #{agent_token.token}" })
+ send_request(params: { gitops_sync_count: 10 })
expect(response).to have_gitlab_http_status(:no_content)
end
it 'returns no_content 0 gitops_sync_count' do
- send_request(params: { gitops_sync_count: 0 }, headers: { 'Authorization' => "Bearer #{agent_token.token}" })
+ send_request(params: { gitops_sync_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' }, headers: { 'Authorization' => "Bearer #{agent_token.token}" })
+ send_request(params: { gitops_sync_count: 'string' })
expect(response).to have_gitlab_http_status(:bad_request)
end
it 'returns 400 for negative number' do
- send_request(params: { gitops_sync_count: '-1' }, headers: { 'Authorization' => "Bearer #{agent_token.token}" })
+ send_request(params: { gitops_sync_count: '-1' })
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -125,6 +125,36 @@ RSpec.describe API::Internal::Kubernetes do
)
)
end
+
+ context 'on GitLab.com' do
+ before do
+ allow(::Gitlab).to receive(:com?).and_return(true)
+ end
+
+ context 'kubernetes_agent_on_gitlab_com feature flag disabled' do
+ before do
+ stub_feature_flags(kubernetes_agent_on_gitlab_com: false)
+ end
+
+ it 'returns 403' do
+ send_request(headers: { 'Authorization' => "Bearer #{agent_token.token}" })
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'kubernetes_agent_on_gitlab_com feature flag enabled' do
+ before do
+ stub_feature_flags(kubernetes_agent_on_gitlab_com: agent_token.agent.project)
+ end
+
+ it 'returns success' do
+ send_request(headers: { 'Authorization' => "Bearer #{agent_token.token}" })
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
+ end
+ end
end
end
@@ -174,6 +204,36 @@ RSpec.describe API::Internal::Kubernetes do
expect(response).to have_gitlab_http_status(:not_found)
end
end
+
+ context 'on GitLab.com' do
+ before do
+ allow(::Gitlab).to receive(:com?).and_return(true)
+ end
+
+ context 'kubernetes_agent_on_gitlab_com feature flag disabled' do
+ before do
+ stub_feature_flags(kubernetes_agent_on_gitlab_com: false)
+ end
+
+ it 'returns 403' do
+ send_request(params: { id: project.id }, headers: { 'Authorization' => "Bearer #{agent_token.token}" })
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'kubernetes_agent_on_gitlab_com feature flag enabled' do
+ before do
+ stub_feature_flags(kubernetes_agent_on_gitlab_com: agent_token.agent.project)
+ end
+
+ it 'returns success' do
+ send_request(params: { id: project.id }, headers: { 'Authorization' => "Bearer #{agent_token.token}" })
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
+ end
+ end
end
context 'project is private' do
diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb
index 6f854a28cec..1c43ef25f14 100644
--- a/spec/requests/api/jobs_spec.rb
+++ b/spec/requests/api/jobs_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe API::Jobs do
end
let!(:job) do
- create(:ci_build, :success, pipeline: pipeline,
+ create(:ci_build, :success, :tags, pipeline: pipeline,
artifacts_expire_at: 1.day.since)
end
@@ -50,6 +50,7 @@ RSpec.describe API::Jobs do
expect(json_response).not_to be_empty
expect(json_response.first['commit']['id']).to eq project.commit.id
expect(Time.parse(json_response.first['artifacts_expire_at'])).to be_like_time(job.artifacts_expire_at)
+ expect(json_response.first['tag_list'].sort).to eq job.tag_list.sort
end
context 'without artifacts and trace' do
diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb
index 6db6de4b533..e3fffd3e3fd 100644
--- a/spec/requests/api/labels_spec.rb
+++ b/spec/requests/api/labels_spec.rb
@@ -10,14 +10,19 @@ RSpec.describe API::Labels do
else
label_id = spec_params[:name] || spec_params[:label_id]
- put api("/projects/#{project.id}/labels/#{label_id}", user),
+ put api("/projects/#{project.id}/labels/#{ERB::Util.url_encode(label_id)}", user),
params: request_params.merge(spec_params.except(:name, :id))
end
end
+ let_it_be(:valid_label_title_1) { 'Label foo & bar:subgroup::v.1' }
+ let_it_be(:valid_label_title_1_esc) { ERB::Util.url_encode(valid_label_title_1) }
+ let_it_be(:valid_label_title_2) { 'Label bar & foo:subgroup::v.2' }
+ let_it_be(:valid_group_label_title_1) { 'Group label foobar:sub::v.1' }
+
let(:user) { create(:user) }
let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
- let!(:label1) { create(:label, description: 'the best label', title: 'label1', project: project) }
+ let!(:label1) { create(:label, description: 'the best label v.1', title: valid_label_title_1, project: project) }
let!(:priority_label) { create(:label, title: 'bug', project: project, priority: 3) }
route_types = [:deprecated, :rest]
@@ -25,10 +30,10 @@ RSpec.describe API::Labels do
shared_examples 'label update API' do
route_types.each do |route_type|
it "returns 200 if name is changed (#{route_type} route)" do
- put_labels_api(route_type, user, spec_params, new_name: 'New Label')
+ put_labels_api(route_type, user, spec_params, new_name: valid_label_title_2)
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['name']).to eq('New Label')
+ expect(json_response['name']).to eq(valid_label_title_2)
expect(json_response['color']).to eq(label1.color)
end
@@ -77,10 +82,10 @@ RSpec.describe API::Labels do
end
it "returns 200 if name and colors and description are changed (#{route_type} route)" do
- put_labels_api(route_type, user, spec_params, new_name: 'New Label', color: '#FFFFFF', description: 'test')
+ put_labels_api(route_type, user, spec_params, new_name: valid_label_title_2, color: '#FFFFFF', description: 'test')
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['name']).to eq('New Label')
+ expect(json_response['name']).to eq(valid_label_title_2)
expect(json_response['color']).to eq('#FFFFFF')
expect(json_response['description']).to eq('test')
end
@@ -141,7 +146,7 @@ RSpec.describe API::Labels do
priority: nil
}.merge(spec_params.except(:name, :id))
- put api("/projects/#{project.id}/labels/#{label_id}", user),
+ put api("/projects/#{project.id}/labels/#{ERB::Util.url_encode(label_id)}", user),
params: request_params
expect(response).to have_gitlab_http_status(:ok)
@@ -167,7 +172,7 @@ RSpec.describe API::Labels do
it 'returns 204 for existing label (rest route)' do
label_id = spec_params[:name] || spec_params[:label_id]
- delete api("/projects/#{project.id}/labels/#{label_id}", user), params: spec_params.except(:name, :label_id)
+ delete api("/projects/#{project.id}/labels/#{ERB::Util.url_encode(label_id)}", user), params: spec_params.except(:name, :label_id)
expect(response).to have_gitlab_http_status(:no_content)
end
@@ -179,7 +184,7 @@ RSpec.describe API::Labels do
describe 'GET /projects/:id/labels' do
let_it_be(:group) { create(:group) }
- let_it_be(:group_label) { create(:group_label, title: 'feature label', group: group) }
+ let_it_be(:group_label) { create(:group_label, title: valid_group_label_title_1, group: group) }
before do
project.update!(group: group)
@@ -219,7 +224,7 @@ RSpec.describe API::Labels do
'closed_issues_count' => 1,
'open_merge_requests_count' => 0,
'name' => label1.name,
- 'description' => 'the best label',
+ 'description' => label1.description,
'color' => a_string_matching(/^#\h{6}$/),
'text_color' => a_string_matching(/^#\h{6}$/),
'priority' => nil,
@@ -293,14 +298,14 @@ RSpec.describe API::Labels do
it 'returns created label when all params' do
post api("/projects/#{project.id}/labels", user),
params: {
- name: 'Foo',
+ name: valid_label_title_2,
color: '#FFAABB',
description: 'test',
priority: 2
}
expect(response).to have_gitlab_http_status(:created)
- expect(json_response['name']).to eq('Foo')
+ expect(json_response['name']).to eq(valid_label_title_2)
expect(json_response['color']).to eq('#FFAABB')
expect(json_response['description']).to eq('test')
expect(json_response['priority']).to eq(2)
@@ -309,12 +314,12 @@ RSpec.describe API::Labels do
it 'returns created label when only required params' do
post api("/projects/#{project.id}/labels", user),
params: {
- name: 'Foo & Bar',
+ name: valid_label_title_2,
color: '#FFAABB'
}
expect(response).to have_gitlab_http_status(:created)
- expect(json_response['name']).to eq('Foo & Bar')
+ expect(json_response['name']).to eq(valid_label_title_2)
expect(json_response['color']).to eq('#FFAABB')
expect(json_response['description']).to be_nil
expect(json_response['priority']).to be_nil
@@ -323,13 +328,13 @@ RSpec.describe API::Labels do
it 'creates a prioritized label' do
post api("/projects/#{project.id}/labels", user),
params: {
- name: 'Foo & Bar',
+ name: valid_label_title_2,
color: '#FFAABB',
priority: 3
}
expect(response).to have_gitlab_http_status(:created)
- expect(json_response['name']).to eq('Foo & Bar')
+ expect(json_response['name']).to eq(valid_label_title_2)
expect(json_response['color']).to eq('#FFAABB')
expect(json_response['description']).to be_nil
expect(json_response['priority']).to eq(3)
@@ -348,7 +353,7 @@ RSpec.describe API::Labels do
it 'returns 400 for invalid color' do
post api("/projects/#{project.id}/labels", user),
params: {
- name: 'Foo',
+ name: valid_label_title_2,
color: '#FFAA'
}
expect(response).to have_gitlab_http_status(:bad_request)
@@ -358,7 +363,7 @@ RSpec.describe API::Labels do
it 'returns 400 for too long color code' do
post api("/projects/#{project.id}/labels", user),
params: {
- name: 'Foo',
+ name: valid_label_title_2,
color: '#FFAAFFFF'
}
expect(response).to have_gitlab_http_status(:bad_request)
@@ -393,7 +398,7 @@ RSpec.describe API::Labels do
it 'returns 400 for invalid priority' do
post api("/projects/#{project.id}/labels", user),
params: {
- name: 'Foo',
+ name: valid_label_title_2,
color: '#FFAAFFFF',
priority: 'foo'
}
@@ -404,7 +409,7 @@ RSpec.describe API::Labels do
it 'returns 409 if label already exists in project' do
post api("/projects/#{project.id}/labels", user),
params: {
- name: 'label1',
+ name: valid_label_title_1,
color: '#FFAABB'
}
expect(response).to have_gitlab_http_status(:conflict)
@@ -414,7 +419,7 @@ RSpec.describe API::Labels do
describe 'DELETE /projects/:id/labels' do
it_behaves_like 'label delete API' do
- let(:spec_params) { { name: 'label1' } }
+ let(:spec_params) { { name: valid_label_title_1 } }
end
it_behaves_like 'label delete API' do
@@ -422,7 +427,7 @@ RSpec.describe API::Labels do
end
it 'returns 404 for non existing label' do
- delete api("/projects/#{project.id}/labels", user), params: { name: 'label2' }
+ delete api("/projects/#{project.id}/labels", user), params: { name: 'unknown' }
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 Label Not Found')
@@ -446,14 +451,14 @@ RSpec.describe API::Labels do
it_behaves_like '412 response' do
let(:request) { api("/projects/#{project.id}/labels", user) }
- let(:params) { { name: 'label1' } }
+ let(:params) { { name: valid_label_title_1 } }
end
end
describe 'PUT /projects/:id/labels' do
context 'when using name' do
it_behaves_like 'label update API' do
- let(:spec_params) { { name: 'label1' } }
+ let(:spec_params) { { name: valid_label_title_1 } }
let(:expected_response_label_id) { label1.id }
end
end
@@ -468,7 +473,7 @@ RSpec.describe API::Labels do
it 'returns 404 if label does not exist' do
put api("/projects/#{project.id}/labels", user),
params: {
- name: 'label2',
+ name: valid_label_title_2,
new_name: 'label3'
}
@@ -571,7 +576,7 @@ RSpec.describe API::Labels do
describe "POST /projects/:id/labels/:label_id/subscribe" do
context "when label_id is a label title" do
it "subscribes to the label" do
- post api("/projects/#{project.id}/labels/#{label1.title}/subscribe", user)
+ post api("/projects/#{project.id}/labels/#{valid_label_title_1_esc}/subscribe", user)
expect(response).to have_gitlab_http_status(:created)
expect(json_response["name"]).to eq(label1.title)
@@ -617,7 +622,7 @@ RSpec.describe API::Labels do
context "when label_id is a label title" do
it "unsubscribes from the label" do
- post api("/projects/#{project.id}/labels/#{label1.title}/unsubscribe", user)
+ post api("/projects/#{project.id}/labels/#{valid_label_title_1_esc}/unsubscribe", user)
expect(response).to have_gitlab_http_status(:created)
expect(json_response["name"]).to eq(label1.title)
diff --git a/spec/requests/api/maven_packages_spec.rb b/spec/requests/api/maven_packages_spec.rb
index e5d11fb1218..7f0e4f18e3b 100644
--- a/spec/requests/api/maven_packages_spec.rb
+++ b/spec/requests/api/maven_packages_spec.rb
@@ -4,6 +4,8 @@ require 'spec_helper'
RSpec.describe API::MavenPackages do
include WorkhorseHelpers
+ include_context 'workhorse headers'
+
let_it_be_with_refind(:package_settings) { create(:namespace_package_setting, :group) }
let_it_be(:group) { package_settings.namespace }
let_it_be(:user) { create(:user) }
@@ -20,8 +22,7 @@ RSpec.describe API::MavenPackages do
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: deploy_token_for_group, group: group) }
let(:package_name) { 'com/example/my-app' }
- let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
- let(:headers) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
+ let(:headers) { workhorse_headers }
let(:headers_with_token) { headers.merge('Private-Token' => personal_access_token.token) }
let(:group_deploy_token_headers) { { Gitlab::Auth::AuthFinders::DEPLOY_TOKEN_HEADER => deploy_token_for_group.token } }
@@ -32,6 +33,7 @@ RSpec.describe API::MavenPackages do
end
let(:version) { '1.0-SNAPSHOT' }
+ let(:param_path) { "#{package_name}/#{version}"}
before do
project.add_developer(user)
@@ -547,8 +549,8 @@ RSpec.describe API::MavenPackages do
end
describe 'PUT /api/v4/projects/:id/packages/maven/*path/:file_name' do
- let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
- let(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
+ include_context 'workhorse headers'
+
let(:send_rewritten_field) { true }
let(:file_upload) { fixture_file_upload('spec/fixtures/packages/maven/my-app-1.0-20180724.124855-1.jar') }
@@ -601,7 +603,7 @@ RSpec.describe API::MavenPackages do
end
context 'without workhorse header' do
- let(:workhorse_header) { {} }
+ let(:workhorse_headers) { {} }
subject { upload_file_with_token(params: params) }
@@ -695,6 +697,14 @@ RSpec.describe API::MavenPackages do
expect(json_response['message']).to include('Duplicate package is not allowed')
end
+ context 'when uploading to the versionless package which contains metadata about all versions' do
+ let(:version) { nil }
+ let(:param_path) { package_name }
+ let!(:package) { create(:maven_package, project: project, version: version, name: project.full_path) }
+
+ it_behaves_like 'storing the package file'
+ end
+
context 'when uploading different non-duplicate files to the same package' do
let!(:package) { create(:maven_package, project: project, name: project.full_path) }
@@ -744,7 +754,7 @@ RSpec.describe API::MavenPackages do
end
def upload_file(params: {}, request_headers: headers, file_extension: 'jar')
- url = "/projects/#{project.id}/packages/maven/#{package_name}/#{version}/my-app-1.0-20180724.124855-1.#{file_extension}"
+ url = "/projects/#{project.id}/packages/maven/#{param_path}/my-app-1.0-20180724.124855-1.#{file_extension}"
workhorse_finalize(
api(url),
method: :put,
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index a04867658e8..ad8e21bf4c1 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -1322,7 +1322,16 @@ RSpec.describe API::MergeRequests do
end
context 'Work in Progress' do
- let!(:merge_request_wip) { create(:merge_request, author: user, assignees: [user], source_project: project, target_project: project, title: "WIP: Test", created_at: base_time + 1.second) }
+ let!(:merge_request_wip) do
+ create(:merge_request,
+ author: user,
+ assignees: [user],
+ source_project: project,
+ target_project: project,
+ title: "WIP: Test",
+ created_at: base_time + 1.second
+ )
+ end
it "returns merge request" do
get api("/projects/#{project.id}/merge_requests/#{merge_request_wip.iid}", user)
@@ -1566,9 +1575,9 @@ RSpec.describe API::MergeRequests do
end
end
- context 'when access_raw_diffs is passed as an option' do
+ context 'when access_raw_diffs is true' do
it_behaves_like 'accesses diffs via raw_diffs' do
- let(:params) { { access_raw_diffs: true } }
+ let(:params) { { access_raw_diffs: "true" } }
end
end
end
@@ -1766,6 +1775,36 @@ RSpec.describe API::MergeRequests do
end
end
+ context 'accepts reviewer_ids' do
+ let(:params) do
+ {
+ title: 'Test merge request',
+ source_branch: 'feature_conflict',
+ target_branch: 'master',
+ author_id: user.id,
+ reviewer_ids: [user2.id]
+ }
+ end
+
+ it 'creates a new merge request with a reviewer' do
+ post api("/projects/#{project.id}/merge_requests", user), params: params
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['title']).to eq('Test merge request')
+ expect(json_response['reviewers'].first['name']).to eq(user2.name)
+ end
+
+ it 'creates a new merge request with no reviewer' do
+ params[:reviewer_ids] = []
+
+ post api("/projects/#{project.id}/merge_requests", user), params: params
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['title']).to eq('Test merge request')
+ expect(json_response['reviewers']).to be_empty
+ end
+ end
+
context 'between branches projects' do
context 'different labels' do
let(:params) do
@@ -2111,6 +2150,34 @@ RSpec.describe API::MergeRequests do
it_behaves_like 'issuable update endpoint' do
let(:entity) { merge_request }
end
+
+ context 'accepts reviewer_ids' do
+ let(:params) do
+ {
+ title: 'Updated merge request',
+ reviewer_ids: [user2.id]
+ }
+ end
+
+ it 'adds a reviewer to the existing merge request' do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['title']).to eq('Updated merge request')
+ expect(json_response['reviewers'].first['name']).to eq(user2.name)
+ end
+
+ it 'removes a reviewer from the existing merge request' do
+ merge_request.reviewers = [user2]
+ params[:reviewer_ids] = []
+
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['title']).to eq('Updated merge request')
+ expect(json_response['reviewers']).to be_empty
+ end
+ end
end
describe "POST /projects/:id/merge_requests/:merge_request_iid/context_commits" do
diff --git a/spec/requests/api/npm_instance_packages_spec.rb b/spec/requests/api/npm_instance_packages_spec.rb
index 8299717b5c7..70c76067a6e 100644
--- a/spec/requests/api/npm_instance_packages_spec.rb
+++ b/spec/requests/api/npm_instance_packages_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe API::NpmInstancePackages do
include_context 'npm api setup'
describe 'GET /api/v4/packages/npm/*package_name' do
- it_behaves_like 'handling get metadata requests' do
+ it_behaves_like 'handling get metadata requests', scope: :instance do
let(:url) { api("/packages/npm/#{package_name}") }
end
end
diff --git a/spec/requests/api/npm_project_packages_spec.rb b/spec/requests/api/npm_project_packages_spec.rb
index 1421f20ac28..7ea238c0607 100644
--- a/spec/requests/api/npm_project_packages_spec.rb
+++ b/spec/requests/api/npm_project_packages_spec.rb
@@ -6,25 +6,25 @@ 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' do
+ it_behaves_like 'handling get metadata requests', scope: :project do
let(:url) { api("/projects/#{project.id}/packages/npm/#{package_name}") }
end
end
describe 'GET /api/v4/projects/:id/packages/npm/-/package/*package_name/dist-tags' do
- it_behaves_like 'handling get dist tags requests' 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
end
describe 'PUT /api/v4/projects/:id/packages/npm/-/package/*package_name/dist-tags/:tag' do
- it_behaves_like 'handling create dist tag requests' do
+ it_behaves_like 'handling create dist tag requests', scope: :project do
let(:url) { api("/projects/#{project.id}/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") }
end
end
describe 'DELETE /api/v4/projects/:id/packages/npm/-/package/*package_name/dist-tags/:tag' do
- it_behaves_like 'handling delete dist tag requests' do
+ it_behaves_like 'handling delete dist tag requests', scope: :project do
let(:url) { api("/projects/#{project.id}/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") }
end
end
@@ -32,10 +32,14 @@ RSpec.describe API::NpmProjectPackages do
describe 'GET /api/v4/projects/:id/packages/npm/*package_name/-/*file_name' do
let_it_be(:package_file) { package.package_files.first }
- let(:params) { {} }
- let(:url) { api("/projects/#{project.id}/packages/npm/#{package_file.package.name}/-/#{package_file.file_name}") }
+ let(:headers) { {} }
+ let(:url) { api("/projects/#{project.id}/packages/npm/#{package.name}/-/#{package_file.file_name}") }
- subject { get(url, params: params) }
+ subject { get(url, headers: headers) }
+
+ before do
+ project.add_developer(user)
+ end
shared_examples 'a package file that requires auth' do
it 'denies download with no token' do
@@ -45,7 +49,7 @@ RSpec.describe API::NpmProjectPackages do
end
context 'with access token' do
- let(:params) { { access_token: token.token } }
+ let(:headers) { build_token_auth_header(token.token) }
it 'returns the file' do
subject
@@ -56,7 +60,7 @@ RSpec.describe API::NpmProjectPackages do
end
context 'with job token' do
- let(:params) { { job_token: job.token } }
+ let(:headers) { build_token_auth_header(job.token) }
it 'returns the file' do
subject
@@ -86,7 +90,7 @@ RSpec.describe API::NpmProjectPackages do
it_behaves_like 'a package file that requires auth'
context 'with guest' do
- let(:params) { { access_token: token.token } }
+ let(:headers) { build_token_auth_header(token.token) }
it 'denies download when not enough permissions' do
project.add_guest(user)
@@ -108,7 +112,11 @@ RSpec.describe API::NpmProjectPackages do
end
describe 'PUT /api/v4/projects/:id/packages/npm/:package_name' do
- RSpec.shared_examples 'handling invalid record with 400 error' do
+ before do
+ project.add_developer(user)
+ end
+
+ shared_examples 'handling invalid record with 400 error' do
it 'handles an ActiveRecord::RecordInvalid exception with 400 error' do
expect { upload_package_with_token(package_name, params) }
.not_to change { project.packages.count }
@@ -261,7 +269,9 @@ RSpec.describe API::NpmProjectPackages do
end
def upload_package(package_name, params = {})
- put api("/projects/#{project.id}/packages/npm/#{package_name.sub('/', '%2f')}"), params: params
+ token = params.delete(:access_token) || params.delete(:job_token)
+ headers = build_token_auth_header(token)
+ put api("/projects/#{project.id}/packages/npm/#{package_name.sub('/', '%2f')}"), params: params, headers: headers
end
def upload_package_with_token(package_name, params = {})
diff --git a/spec/requests/api/nuget_project_packages_spec.rb b/spec/requests/api/nuget_project_packages_spec.rb
index 813ebc35ede..0277aa73220 100644
--- a/spec/requests/api/nuget_project_packages_spec.rb
+++ b/spec/requests/api/nuget_project_packages_spec.rb
@@ -144,8 +144,8 @@ RSpec.describe API::NugetProjectPackages do
end
describe 'PUT /api/v4/projects/:id/packages/nuget/authorize' do
- let_it_be(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
- let_it_be(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
+ include_context 'workhorse headers'
+
let(:url) { "/projects/#{target.id}/packages/nuget/authorize" }
let(:headers) { {} }
@@ -176,7 +176,7 @@ RSpec.describe API::NugetProjectPackages do
with_them do
let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
- let(:headers) { user_headers.merge(workhorse_header) }
+ let(:headers) { user_headers.merge(workhorse_headers) }
before do
update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false))
@@ -194,8 +194,8 @@ RSpec.describe API::NugetProjectPackages do
end
describe 'PUT /api/v4/projects/:id/packages/nuget' do
- let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
- let(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
+ include_context 'workhorse headers'
+
let_it_be(:file_name) { 'package.nupkg' }
let(:url) { "/projects/#{target.id}/packages/nuget" }
let(:headers) { {} }
@@ -239,7 +239,7 @@ RSpec.describe API::NugetProjectPackages do
with_them do
let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
- let(:headers) { user_headers.merge(workhorse_header) }
+ let(:headers) { user_headers.merge(workhorse_headers) }
before do
update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false))
@@ -256,7 +256,7 @@ RSpec.describe API::NugetProjectPackages do
it_behaves_like 'rejects nuget access with invalid target id'
context 'file size above maximum limit' do
- let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_header) }
+ let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_headers) }
before do
allow_next_instance_of(UploadedFile) do |uploaded_file|
diff --git a/spec/requests/api/oauth_tokens_spec.rb b/spec/requests/api/oauth_tokens_spec.rb
index 23d5df873d4..52c7408545f 100644
--- a/spec/requests/api/oauth_tokens_spec.rb
+++ b/spec/requests/api/oauth_tokens_spec.rb
@@ -26,17 +26,14 @@ RSpec.describe 'OAuth tokens' do
end
context 'when user does not have 2FA enabled' do
- # NOTE: using ROPS grant flow without client credentials will be deprecated
- # and removed in the next version of Doorkeeper.
- # See https://gitlab.com/gitlab-org/gitlab/-/issues/219137
context 'when no client credentials provided' do
- it 'creates an access token' do
+ it 'does not create an access token' do
user = create(:user)
request_oauth_token(user)
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['access_token']).not_to be_nil
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ expect(json_response['access_token']).to be_nil
end
end
@@ -54,15 +51,11 @@ RSpec.describe 'OAuth tokens' do
context 'with invalid credentials' do
it 'does not create an access token' do
- # NOTE: remove this after update to Doorkeeper 5.5 or newer, see
- # https://gitlab.com/gitlab-org/gitlab/-/issues/219137
- pending 'Enable this example after upgrading Doorkeeper to 5.5 or newer'
-
user = create(:user)
request_oauth_token(user, basic_auth_header(client.uid, 'invalid secret'))
- expect(response).to have_gitlab_http_status(:bad_request)
+ expect(response).to have_gitlab_http_status(:unauthorized)
expect(json_response['error']).to eq('invalid_client')
end
end
diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml
new file mode 100644
index 00000000000..181fcafd577
--- /dev/null
+++ b/spec/requests/api/project_attributes.yml
@@ -0,0 +1,149 @@
+---
+itself: # project
+ unexposed_attributes:
+ - bfg_object_map
+ - delete_error
+ - detected_repository_languages
+ - disable_overriding_approvers_per_merge_request
+ - external_authorization_classification_label
+ - external_webhook_token
+ - has_external_issue_tracker
+ - has_external_wiki
+ - import_source
+ - import_type
+ - import_url
+ - issues_template
+ - jobs_cache_index
+ - last_repository_check_at
+ - last_repository_check_failed
+ - last_repository_updated_at
+ - marked_for_deletion_at
+ - marked_for_deletion_by_user_id
+ - max_artifacts_size
+ - max_pages_size
+ - merge_requests_author_approval
+ - merge_requests_disable_committers_approval
+ - merge_requests_rebase_enabled
+ - merge_requests_template
+ - mirror_last_successful_update_at
+ - mirror_last_update_at
+ - mirror_overwrites_diverged_branches
+ - mirror_trigger_builds
+ - mirror_user_id
+ - only_mirror_protected_branches
+ - pages_https_only
+ - pending_delete
+ - pool_repository_id
+ - pull_mirror_available_overridden
+ - pull_mirror_branch_prefix
+ - remote_mirror_available_overridden
+ - repository_read_only
+ - repository_size_limit
+ - require_password_to_approve
+ - reset_approvals_on_push
+ - runners_token_encrypted
+ - storage_version
+ - updated_at
+ remapped_attributes:
+ avatar: avatar_url
+ build_allow_git_fetch: build_git_strategy
+ merge_requests_ff_only_enabled: merge_method
+ namespace_id: namespace
+ public_builds: public_jobs
+ visibility_level: visibility
+ computed_attributes:
+ - _links
+ - can_create_merge_request_in
+ - compliance_frameworks
+ - container_expiration_policy
+ - default_branch
+ - empty_repo
+ - forks_count
+ - http_url_to_repo
+ - name_with_namespace
+ - open_issues_count
+ - owner
+ - path_with_namespace
+ - permissions
+ - readme_url
+ - shared_with_groups
+ - ssh_url_to_repo
+ - web_url
+
+build_auto_devops: # auto_devops
+ unexposed_attributes:
+ - id
+ - project_id
+ - created_at
+ - updated_at
+ remapped_attributes:
+ enabled: auto_devops_enabled
+ deploy_strategy: auto_devops_deploy_strategy
+
+ci_cd_settings:
+ unexposed_attributes:
+ - id
+ - project_id
+ - group_runners_enabled
+ - keep_latest_artifact
+ - merge_pipelines_enabled
+ - merge_trains_enabled
+ - auto_rollback_enabled
+ remapped_attributes:
+ default_git_depth: ci_default_git_depth
+ forward_deployment_enabled: ci_forward_deployment_enabled
+
+build_import_state: # import_state
+ unexposed_attributes:
+ - id
+ - project_id
+ - retry_count
+ - last_update_started_at
+ - last_update_scheduled_at
+ - next_execution_timestamp
+ - jid
+ - last_update_at
+ - last_successful_update_at
+ - correlation_id_value
+ remapped_attributes:
+ status: import_status
+ last_error: import_error
+
+project_feature:
+ unexposed_attributes:
+ - id
+ - created_at
+ - metrics_dashboard_access_level
+ - project_id
+ - requirements_access_level
+ - security_and_compliance_access_level
+ - updated_at
+ computed_attributes:
+ - issues_enabled
+ - jobs_enabled
+ - merge_requests_enabled
+ - requirements_enabled
+ - security_and_compliance_enabled
+ - snippets_enabled
+ - wiki_enabled
+
+project_setting:
+ unexposed_attributes:
+ - allow_editing_commit_messages
+ - created_at
+ - has_confluence
+ - has_vulnerabilities
+ - prevent_merge_without_jira_issue
+ - project_id
+ - push_rule_id
+ - show_default_award_emojis
+ - squash_option
+ - updated_at
+
+build_service_desk_setting: # service_desk_setting
+ unexposed_attributes:
+ - project_id
+ - issue_template_key
+ - outgoing_name
+ remapped_attributes:
+ project_key: service_desk_address
diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb
index 8e99d37c84f..a049d7d7515 100644
--- a/spec/requests/api/project_import_spec.rb
+++ b/spec/requests/api/project_import_spec.rb
@@ -5,13 +5,12 @@ require 'spec_helper'
RSpec.describe API::ProjectImport do
include WorkhorseHelpers
+ include_context 'workhorse headers'
+
let(:user) { create(:user) }
let(:file) { File.join('spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') }
let(:namespace) { create(:group) }
- let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
- let(:workhorse_headers) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
-
before do
namespace.add_owner(user)
end
diff --git a/spec/requests/api/project_packages_spec.rb b/spec/requests/api/project_packages_spec.rb
index eb86df36dbb..1f3887cab8a 100644
--- a/spec/requests/api/project_packages_spec.rb
+++ b/spec/requests/api/project_packages_spec.rb
@@ -120,6 +120,7 @@ RSpec.describe API::ProjectPackages do
end
it_behaves_like 'with versionless packages'
+ it_behaves_like 'with status param'
it_behaves_like 'does not cause n^2 queries'
end
end
diff --git a/spec/requests/api/project_templates_spec.rb b/spec/requests/api/project_templates_spec.rb
index fc1035fc17d..a424bc62014 100644
--- a/spec/requests/api/project_templates_spec.rb
+++ b/spec/requests/api/project_templates_spec.rb
@@ -131,7 +131,7 @@ RSpec.describe API::ProjectTemplates do
end
end
- describe 'GET /projects/:id/templates/:type/:key' do
+ describe 'GET /projects/:id/templates/:type/:name' do
it 'returns a specific dockerfile' do
get api("/projects/#{public_project.id}/templates/dockerfiles/Binary")
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 8a4a7880ab4..ad36777184a 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -1140,7 +1140,7 @@ RSpec.describe API::Projects do
let!(:public_project) { create(:project, :public, name: 'public_project', creator_id: user4.id, namespace: user4.namespace) }
it 'returns error when user not found' do
- get api('/users/0/projects/')
+ get api("/users/#{non_existing_record_id}/projects/")
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 User Not Found')
@@ -1540,6 +1540,35 @@ RSpec.describe API::Projects do
end
context 'when authenticated as an admin' do
+ let(:project_attributes_file) { 'spec/requests/api/project_attributes.yml' }
+ let(:project_attributes) { YAML.load_file(project_attributes_file) }
+
+ let(:expected_keys) do
+ keys = project_attributes.map do |relation, relation_config|
+ begin
+ actual_keys = project.send(relation).attributes.keys
+ rescue NoMethodError
+ actual_keys = ["#{relation} is nil"]
+ end
+ unexposed_attributes = relation_config['unexposed_attributes'] || []
+ remapped_attributes = relation_config['remapped_attributes'] || {}
+ computed_attributes = relation_config['computed_attributes'] || []
+ actual_keys - unexposed_attributes - remapped_attributes.keys + remapped_attributes.values + computed_attributes
+ end.flatten
+
+ unless Gitlab.ee?
+ keys -= %w[
+ approvals_before_merge
+ compliance_frameworks
+ mirror
+ requirements_enabled
+ security_and_compliance_enabled
+ ]
+ end
+
+ keys
+ end
+
it 'returns a project by id' do
project
project_member
@@ -1588,6 +1617,27 @@ RSpec.describe API::Projects do
expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved)
expect(json_response['operations_access_level']).to be_present
end
+
+ it 'exposes all necessary attributes' do
+ create(:project_group_link, project: project)
+
+ get api("/projects/#{project.id}", admin)
+
+ diff = Set.new(json_response.keys) ^ Set.new(expected_keys)
+
+ expect(diff).to be_empty, failure_message(diff)
+ end
+
+ def failure_message(diff)
+ <<~MSG
+ It looks like project's set of exposed attributes is different from the expected set.
+
+ The following attributes are missing or newly added:
+ #{diff.to_a.to_sentence}
+
+ Please update #{project_attributes_file} file"
+ MSG
+ end
end
context 'when authenticated as a regular user' do
@@ -2155,7 +2205,7 @@ RSpec.describe API::Projects do
end
it 'fails if forked_from project which does not exist' do
- post api("/projects/#{project_fork_target.id}/fork/0", admin)
+ post api("/projects/#{project_fork_target.id}/fork/#{non_existing_record_id}", admin)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -2399,7 +2449,7 @@ RSpec.describe API::Projects do
end
it 'returns a 404 error when project does not exist' do
- delete api("/projects/123/share/#{non_existing_record_id}", user)
+ delete api("/projects/#{non_existing_record_id}/share/#{non_existing_record_id}", user)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -2956,7 +3006,7 @@ RSpec.describe API::Projects do
end
it 'returns the proper security headers' do
- get api('/projects/1/starrers', current_user)
+ get api("/projects/#{public_project.id}/starrers", current_user)
expect(response).to include_security_headers
end
@@ -3029,7 +3079,7 @@ RSpec.describe API::Projects do
end
it 'returns not_found(404) for not existing project' do
- get api("/projects/0/languages", user)
+ get api("/projects/#{non_existing_record_id}/languages", user)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -3080,7 +3130,7 @@ RSpec.describe API::Projects do
end
it 'does not remove a non existing project' do
- delete api('/projects/1328', user)
+ delete api("/projects/#{non_existing_record_id}", user)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -3099,7 +3149,7 @@ RSpec.describe API::Projects do
end
it 'does not remove a non existing project' do
- delete api('/projects/1328', admin)
+ delete api("/projects/#{non_existing_record_id}", admin)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -3329,8 +3379,8 @@ RSpec.describe API::Projects do
expect(json_response['message']['path']).to eq(['has already been taken'])
end
- it 'accepts a name for the target project' do
- post api("/projects/#{project.id}/fork", user2), params: { name: 'My Random Project' }
+ it 'accepts custom parameters for the target project' do
+ post api("/projects/#{project.id}/fork", user2), params: { name: 'My Random Project', description: 'A description', visibility: 'private' }
expect(response).to have_gitlab_http_status(:created)
expect(json_response['name']).to eq('My Random Project')
@@ -3338,6 +3388,8 @@ RSpec.describe API::Projects do
expect(json_response['owner']['id']).to eq(user2.id)
expect(json_response['namespace']['id']).to eq(user2.namespace.id)
expect(json_response['forked_from_project']['id']).to eq(project.id)
+ expect(json_response['description']).to eq('A description')
+ expect(json_response['visibility']).to eq('private')
expect(json_response['import_status']).to eq('scheduled')
expect(json_response).to include("import_error")
end
@@ -3369,6 +3421,13 @@ RSpec.describe API::Projects do
expect(json_response['message']['path']).to eq(['has already been taken'])
expect(json_response['message']['name']).to eq(['has already been taken'])
end
+
+ it 'fails to fork with an unknown visibility level' do
+ post api("/projects/#{project.id}/fork", user2), params: { visibility: 'something' }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq('visibility does not have a valid value')
+ end
end
context 'when unauthenticated' do
diff --git a/spec/requests/api/pypi_packages_spec.rb b/spec/requests/api/pypi_packages_spec.rb
index 72a470dca4b..ae5b132f409 100644
--- a/spec/requests/api/pypi_packages_spec.rb
+++ b/spec/requests/api/pypi_packages_spec.rb
@@ -5,6 +5,7 @@ RSpec.describe API::PypiPackages do
include WorkhorseHelpers
include PackagesManagerApiSpecHelpers
include HttpBasicAuthHelpers
+ using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, :public) }
@@ -20,8 +21,6 @@ RSpec.describe API::PypiPackages do
subject { get api(url) }
context 'with valid project' do
- using RSpec::Parameterized::TableSyntax
-
where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
'PUBLIC' | :developer | true | true | 'PyPI package versions' | :success
'PUBLIC' | :guest | true | true | 'PyPI package versions' | :success
@@ -75,16 +74,14 @@ RSpec.describe API::PypiPackages do
end
describe 'POST /api/v4/projects/:id/packages/pypi/authorize' do
- let_it_be(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
- let_it_be(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
+ include_context 'workhorse headers'
+
let(:url) { "/projects/#{project.id}/packages/pypi/authorize" }
let(:headers) { {} }
subject { post api(url), headers: headers }
context 'with valid project' do
- using RSpec::Parameterized::TableSyntax
-
where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
'PUBLIC' | :developer | true | true | 'process PyPI api request' | :success
'PUBLIC' | :guest | true | true | 'process PyPI api request' | :forbidden
@@ -109,7 +106,7 @@ RSpec.describe API::PypiPackages do
with_them do
let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
- let(:headers) { user_headers.merge(workhorse_header) }
+ let(:headers) { user_headers.merge(workhorse_headers) }
before do
project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false))
@@ -127,8 +124,8 @@ RSpec.describe API::PypiPackages do
end
describe 'POST /api/v4/projects/:id/packages/pypi' do
- let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
- let(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
+ include_context 'workhorse headers'
+
let_it_be(:file_name) { 'package.whl' }
let(:url) { "/projects/#{project.id}/packages/pypi" }
let(:headers) { {} }
@@ -149,8 +146,6 @@ RSpec.describe API::PypiPackages do
end
context 'with valid project' do
- using RSpec::Parameterized::TableSyntax
-
where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
'PUBLIC' | :developer | true | true | 'PyPI package creation' | :created
'PUBLIC' | :guest | true | true | 'process PyPI api request' | :forbidden
@@ -175,7 +170,7 @@ RSpec.describe API::PypiPackages do
with_them do
let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
- let(:headers) { user_headers.merge(workhorse_header) }
+ let(:headers) { user_headers.merge(workhorse_headers) }
before do
project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false))
@@ -189,7 +184,7 @@ RSpec.describe API::PypiPackages do
let(:requires_python) { 'x' * 256 }
let(:token) { personal_access_token.token }
let(:user_headers) { basic_auth_header(user.username, token) }
- let(:headers) { user_headers.merge(workhorse_header) }
+ let(:headers) { user_headers.merge(workhorse_headers) }
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
@@ -201,7 +196,7 @@ RSpec.describe API::PypiPackages do
context 'with an invalid package' do
let(:token) { personal_access_token.token }
let(:user_headers) { basic_auth_header(user.username, token) }
- let(:headers) { user_headers.merge(workhorse_header) }
+ let(:headers) { user_headers.merge(workhorse_headers) }
before do
params[:name] = '.$/@!^*'
@@ -218,7 +213,7 @@ RSpec.describe API::PypiPackages do
it_behaves_like 'rejects PyPI access with unknown project id'
context 'file size above maximum limit' do
- let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_header) }
+ let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_headers) }
before do
allow_next_instance_of(UploadedFile) do |uploaded_file|
@@ -239,8 +234,6 @@ RSpec.describe API::PypiPackages do
subject { get api(url) }
context 'with valid project' do
- using RSpec::Parameterized::TableSyntax
-
where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
'PUBLIC' | :developer | true | true | 'PyPI package download' | :success
'PUBLIC' | :guest | true | true | 'PyPI package download' | :success
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index 45bce8c8a5c..ace73e49c7c 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -610,4 +610,85 @@ RSpec.describe API::Repositories do
end
end
end
+
+ describe 'POST /projects/:id/repository/changelog' do
+ it 'generates the changelog for a version' do
+ spy = instance_spy(Repositories::ChangelogService)
+
+ allow(Repositories::ChangelogService)
+ .to receive(:new)
+ .with(
+ project,
+ user,
+ version: '1.0.0',
+ from: 'foo',
+ to: 'bar',
+ date: DateTime.new(2020, 1, 1),
+ branch: 'kittens',
+ trailer: 'Foo',
+ file: 'FOO.md',
+ message: 'Commit message'
+ )
+ .and_return(spy)
+
+ allow(spy).to receive(:execute)
+
+ post(
+ api("/projects/#{project.id}/repository/changelog", user),
+ params: {
+ version: '1.0.0',
+ from: 'foo',
+ to: 'bar',
+ date: '2020-01-01',
+ branch: 'kittens',
+ trailer: 'Foo',
+ file: 'FOO.md',
+ message: 'Commit message'
+ }
+ )
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'produces an error when generating the changelog fails' do
+ spy = instance_spy(Repositories::ChangelogService)
+
+ allow(Repositories::ChangelogService)
+ .to receive(:new)
+ .with(
+ project,
+ user,
+ version: '1.0.0',
+ from: 'foo',
+ to: 'bar',
+ date: DateTime.new(2020, 1, 1),
+ branch: 'kittens',
+ trailer: 'Foo',
+ file: 'FOO.md',
+ message: 'Commit message'
+ )
+ .and_return(spy)
+
+ allow(spy)
+ .to receive(:execute)
+ .and_raise(Gitlab::Changelog::Error.new('oops'))
+
+ post(
+ api("/projects/#{project.id}/repository/changelog", user),
+ params: {
+ version: '1.0.0',
+ from: 'foo',
+ to: 'bar',
+ date: '2020-01-01',
+ branch: 'kittens',
+ trailer: 'Foo',
+ file: 'FOO.md',
+ message: 'Commit message'
+ }
+ )
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ expect(json_response['message']).to eq('Failed to generate the changelog: oops')
+ end
+ end
end
diff --git a/spec/requests/api/resource_access_tokens_spec.rb b/spec/requests/api/resource_access_tokens_spec.rb
new file mode 100644
index 00000000000..9fd7eb2177d
--- /dev/null
+++ b/spec/requests/api/resource_access_tokens_spec.rb
@@ -0,0 +1,293 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe API::ResourceAccessTokens do
+ context "when the resource is a project" do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:other_project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ describe "GET projects/:id/access_tokens" do
+ subject(:get_tokens) { get api("/projects/#{project_id}/access_tokens", user) }
+
+ context "when the user has maintainer permissions" do
+ let_it_be(:project_bot) { create(:user, :project_bot) }
+ let_it_be(:access_tokens) { create_list(:personal_access_token, 3, user: project_bot) }
+ let_it_be(:project_id) { project.id }
+
+ before do
+ project.add_maintainer(user)
+ project.add_maintainer(project_bot)
+ end
+
+ it "gets a list of access tokens for the specified project" do
+ get_tokens
+
+ token_ids = json_response.map { |token| token['id'] }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(token_ids).to match_array(access_tokens.pluck(:id))
+ end
+
+ context "when using a project access token to GET other project access tokens" do
+ let_it_be(:token) { access_tokens.first }
+
+ it "gets a list of access tokens for the specified project" do
+ get api("/projects/#{project_id}/access_tokens", personal_access_token: token)
+
+ token_ids = json_response.map { |token| token['id'] }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(token_ids).to match_array(access_tokens.pluck(:id))
+ end
+ end
+
+ context "when tokens belong to a different project" do
+ let_it_be(:bot) { create(:user, :project_bot) }
+ let_it_be(:token) { create(:personal_access_token, user: bot) }
+
+ before do
+ other_project.add_maintainer(bot)
+ other_project.add_maintainer(user)
+ end
+
+ it "does not return tokens from a different project" do
+ get_tokens
+
+ token_ids = json_response.map { |token| token['id'] }
+
+ expect(token_ids).not_to include(token.id)
+ end
+ end
+
+ context "when the project has no access tokens" do
+ let(:project_id) { other_project.id }
+
+ before do
+ other_project.add_maintainer(user)
+ end
+
+ it 'returns an empty array' do
+ get_tokens
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq([])
+ end
+ end
+
+ context "when trying to get the tokens of a different project" do
+ let_it_be(:project_id) { other_project.id }
+
+ it "returns 404" do
+ get_tokens
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context "when the project does not exist" do
+ let(:project_id) { non_existing_record_id }
+
+ it "returns 404" do
+ get_tokens
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ context "when the user does not have valid permissions" do
+ let_it_be(:project_bot) { create(:user, :project_bot) }
+ let_it_be(:access_tokens) { create_list(:personal_access_token, 3, user: project_bot) }
+ let_it_be(:project_id) { project.id }
+
+ before do
+ project.add_developer(user)
+ project.add_maintainer(project_bot)
+ end
+
+ it "returns 401" do
+ get_tokens
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+ end
+
+ describe "DELETE projects/:id/access_tokens/:token_id", :sidekiq_inline do
+ subject(:delete_token) { delete api("/projects/#{project_id}/access_tokens/#{token_id}", user) }
+
+ let_it_be(:project_bot) { create(:user, :project_bot) }
+ let_it_be(:token) { create(:personal_access_token, user: project_bot) }
+ let_it_be(:project_id) { project.id }
+ let_it_be(:token_id) { token.id }
+
+ before do
+ project.add_maintainer(project_bot)
+ end
+
+ context "when the user has maintainer permissions" do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it "deletes the project access token from the project" do
+ delete_token
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(User.exists?(project_bot.id)).to be_falsy
+ end
+
+ context "when attempting to delete a non-existent project access token" do
+ let_it_be(:token_id) { non_existing_record_id }
+
+ it "does not delete the token, and returns 404" do
+ delete_token
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(response.body).to include("Could not find project access token with token_id: #{token_id}")
+ end
+ end
+
+ context "when attempting to delete a token that does not belong to the specified project" do
+ let_it_be(:project_id) { other_project.id }
+
+ before do
+ other_project.add_maintainer(user)
+ end
+
+ it "does not delete the token, and returns 404" do
+ delete_token
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(response.body).to include("Could not find project access token with token_id: #{token_id}")
+ end
+ end
+ end
+
+ context "when the user does not have valid permissions" do
+ before do
+ project.add_developer(user)
+ end
+
+ it "does not delete the token, and returns 400", :aggregate_failures do
+ delete_token
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(User.exists?(project_bot.id)).to be_truthy
+ expect(response.body).to include("#{user.name} cannot delete #{token.user.name}")
+ end
+ end
+ end
+
+ describe "POST projects/:id/access_tokens" do
+ let_it_be(:params) { { name: "test", scopes: ["api"], expires_at: Date.today + 1.month } }
+
+ subject(:create_token) { post api("/projects/#{project_id}/access_tokens", user), params: params }
+
+ context "when the user has maintainer permissions" do
+ let_it_be(:project_id) { project.id }
+ let_it_be(:expires_at) { 1.month.from_now }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ context "with valid params" do
+ context "with full params" do
+ it "creates a project access token with the params", :aggregate_failures do
+ create_token
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response["name"]).to eq("test")
+ expect(json_response["scopes"]).to eq(["api"])
+ expect(json_response["expires_at"]).to eq(expires_at.to_date.iso8601)
+ end
+ end
+
+ context "when 'expires_at' is not set" do
+ let_it_be(:params) { { name: "test", scopes: ["api"] } }
+
+ it "creates a project access token with the params", :aggregate_failures do
+ create_token
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response["name"]).to eq("test")
+ expect(json_response["scopes"]).to eq(["api"])
+ expect(json_response["expires_at"]).to eq(nil)
+ end
+ end
+ end
+
+ context "with invalid params" do
+ context "when missing the 'name' param" do
+ let_it_be(:params) { { scopes: ["api"], expires_at: 5.days.from_now } }
+
+ it "does not create a project access token without 'name'" do
+ create_token
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(response.body).to include("name is missing")
+ end
+ end
+
+ context "when missing the 'scopes' param" do
+ let_it_be(:params) { { name: "test", expires_at: 5.days.from_now } }
+
+ it "does not create a project access token without 'scopes'" do
+ create_token
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(response.body).to include("scopes is missing")
+ end
+ end
+ end
+
+ context "when trying to create a token in a different project" do
+ let_it_be(:project_id) { other_project.id }
+
+ it "does not create the token, and returns the project not found error" do
+ create_token
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(response.body).to include("Project Not Found")
+ end
+ end
+ end
+
+ context "when the user does not have valid permissions" do
+ let_it_be(:project_id) { project.id }
+
+ context "when the user is a developer" do
+ before do
+ project.add_developer(user)
+ end
+
+ it "does not create the token, and returns the permission error" do
+ create_token
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(response.body).to include("User does not have permission to create project access token")
+ end
+ end
+
+ context "when a project access token tries to create another project access token" do
+ let_it_be(:project_bot) { create(:user, :project_bot) }
+ let_it_be(:user) { project_bot }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ it "does not allow a project access token to create another project access token" do
+ create_token
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(response.body).to include("User does not have permission to create project access token")
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/rubygem_packages_spec.rb b/spec/requests/api/rubygem_packages_spec.rb
new file mode 100644
index 00000000000..5dd68bf9b10
--- /dev/null
+++ b/spec/requests/api/rubygem_packages_spec.rb
@@ -0,0 +1,139 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::RubygemPackages do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:personal_access_token) { create(:personal_access_token) }
+ let_it_be(:user) { personal_access_token.user }
+ let_it_be(:job) { create(:ci_build, :running, user: user) }
+ let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
+ let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
+ let_it_be(:headers) { {} }
+
+ shared_examples 'when feature flag is disabled' do
+ let(:headers) do
+ { 'HTTP_AUTHORIZATION' => personal_access_token.token }
+ end
+
+ before do
+ stub_feature_flags(rubygem_packages: false)
+ end
+
+ it_behaves_like 'returning response status', :not_found
+ end
+
+ shared_examples 'when package feature is disabled' do
+ before do
+ stub_config(packages: { enabled: false })
+ end
+
+ it_behaves_like 'returning response status', :not_found
+ end
+
+ shared_examples 'without authentication' do
+ it_behaves_like 'returning response status', :unauthorized
+ end
+
+ shared_examples 'with authentication' do
+ let(:headers) do
+ { 'HTTP_AUTHORIZATION' => token }
+ end
+
+ let(:tokens) do
+ {
+ personal_access_token: personal_access_token.token,
+ deploy_token: deploy_token.token,
+ job_token: job.token
+ }
+ end
+
+ where(:user_role, :token_type, :valid_token, :status) do
+ :guest | :personal_access_token | true | :not_found
+ :guest | :personal_access_token | false | :unauthorized
+ :guest | :deploy_token | true | :not_found
+ :guest | :deploy_token | false | :unauthorized
+ :guest | :job_token | true | :not_found
+ :guest | :job_token | false | :unauthorized
+ :reporter | :personal_access_token | true | :not_found
+ :reporter | :personal_access_token | false | :unauthorized
+ :reporter | :deploy_token | true | :not_found
+ :reporter | :deploy_token | false | :unauthorized
+ :reporter | :job_token | true | :not_found
+ :reporter | :job_token | false | :unauthorized
+ :developer | :personal_access_token | true | :not_found
+ :developer | :personal_access_token | false | :unauthorized
+ :developer | :deploy_token | true | :not_found
+ :developer | :deploy_token | false | :unauthorized
+ :developer | :job_token | true | :not_found
+ :developer | :job_token | false | :unauthorized
+ end
+
+ with_them do
+ before do
+ project.send("add_#{user_role}", user) unless user_role == :anonymous
+ end
+
+ let(:token) { valid_token ? tokens[token_type] : 'invalid-token123' }
+
+ it_behaves_like 'returning response status', params[:status]
+ end
+ end
+
+ shared_examples 'an unimplemented route' do
+ it_behaves_like 'without authentication'
+ it_behaves_like 'with authentication'
+ it_behaves_like 'when feature flag is disabled'
+ it_behaves_like 'when package feature is disabled'
+ end
+
+ describe 'GET /api/v4/projects/:project_id/packages/rubygems/:filename' do
+ let(:url) { api("/projects/#{project.id}/packages/rubygems/specs.4.8.gz") }
+
+ subject { get(url, headers: headers) }
+
+ it_behaves_like 'an unimplemented route'
+ end
+
+ describe 'GET /api/v4/projects/:project_id/packages/rubygems/quick/Marshal.4.8/:file_name' do
+ let(:url) { api("/projects/#{project.id}/packages/rubygems/quick/Marshal.4.8/my_gem-1.0.0.gemspec.rz") }
+
+ subject { get(url, headers: headers) }
+
+ it_behaves_like 'an unimplemented route'
+ end
+
+ describe 'GET /api/v4/projects/:project_id/packages/rubygems/gems/:file_name' do
+ let(:url) { api("/projects/#{project.id}/packages/rubygems/gems/my_gem-1.0.0.gem") }
+
+ subject { get(url, headers: headers) }
+
+ it_behaves_like 'an unimplemented route'
+ end
+
+ describe 'POST /api/v4/projects/:project_id/packages/rubygems/api/v1/gems/authorize' do
+ let(:url) { api("/projects/#{project.id}/packages/rubygems/api/v1/gems/authorize") }
+
+ subject { post(url, headers: headers) }
+
+ it_behaves_like 'an unimplemented route'
+ end
+
+ describe 'POST /api/v4/projects/:project_id/packages/rubygems/api/v1/gems' do
+ let(:url) { api("/projects/#{project.id}/packages/rubygems/api/v1/gems") }
+
+ subject { post(url, headers: headers) }
+
+ it_behaves_like 'an unimplemented route'
+ end
+
+ describe 'GET /api/v4/projects/:project_id/packages/rubygems/api/v1/dependencies' do
+ let(:url) { api("/projects/#{project.id}/packages/rubygems/api/v1/dependencies") }
+
+ subject { get(url, headers: headers) }
+
+ it_behaves_like 'an unimplemented route'
+ end
+end
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 8fb0f8fc51a..3b84c812010 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -199,6 +199,14 @@ RSpec.describe API::Settings, 'Settings' do
expect(json_response['allow_local_requests_from_hooks_and_services']).to eq(true)
end
+ it 'supports legacy asset_proxy_whitelist' do
+ put api("/application/settings", admin),
+ params: { asset_proxy_whitelist: ['example.com', '*.example.com'] }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['asset_proxy_allowlist']).to eq(['example.com', '*.example.com', 'localhost'])
+ end
+
it 'disables ability to switch to legacy storage' do
put api("/application/settings", admin),
params: { hashed_storage_enabled: false }
@@ -362,24 +370,24 @@ RSpec.describe API::Settings, 'Settings' do
asset_proxy_enabled: true,
asset_proxy_url: 'http://assets.example.com',
asset_proxy_secret_key: 'shared secret',
- asset_proxy_whitelist: ['example.com', '*.example.com']
+ asset_proxy_allowlist: ['example.com', '*.example.com']
}
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['asset_proxy_enabled']).to be(true)
expect(json_response['asset_proxy_url']).to eq('http://assets.example.com')
expect(json_response['asset_proxy_secret_key']).to be_nil
- expect(json_response['asset_proxy_whitelist']).to eq(['example.com', '*.example.com', 'localhost'])
+ expect(json_response['asset_proxy_allowlist']).to eq(['example.com', '*.example.com', 'localhost'])
end
- it 'allows a string for asset_proxy_whitelist' do
+ it 'allows a string for asset_proxy_allowlist' do
put api('/application/settings', admin),
params: {
- asset_proxy_whitelist: 'example.com, *.example.com'
+ asset_proxy_allowlist: 'example.com, *.example.com'
}
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['asset_proxy_whitelist']).to eq(['example.com', '*.example.com', 'localhost'])
+ expect(json_response['asset_proxy_allowlist']).to eq(['example.com', '*.example.com', 'localhost'])
end
end
diff --git a/spec/requests/api/suggestions_spec.rb b/spec/requests/api/suggestions_spec.rb
index 78a2688ac5e..7f53d379af5 100644
--- a/spec/requests/api/suggestions_spec.rb
+++ b/spec/requests/api/suggestions_spec.rb
@@ -65,6 +65,19 @@ RSpec.describe API::Suggestions do
end
end
+ context 'when a custom commit message is included' do
+ it 'renders an ok response and returns json content' do
+ project.add_maintainer(user)
+
+ message = "cool custom commit message!"
+
+ put api(url, user), params: { commit_message: message }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(project.repository.commit.message).to eq(message)
+ end
+ end
+
context 'when not able to apply patch' do
let(:url) { "/suggestions/#{unappliable_suggestion.id}/apply" }
@@ -113,9 +126,11 @@ RSpec.describe API::Suggestions do
let(:url) { "/suggestions/batch_apply" }
context 'when successfully applies multiple patches as a batch' do
- it 'renders an ok response and returns json content' do
+ before do
project.add_maintainer(user)
+ end
+ it 'renders an ok response and returns json content' do
put api(url, user), params: { ids: [suggestion.id, suggestion2.id] }
expect(response).to have_gitlab_http_status(:ok)
@@ -123,6 +138,16 @@ RSpec.describe API::Suggestions do
'appliable', 'applied',
'from_content', 'to_content'))
end
+
+ it 'provides a custom commit message' do
+ message = "cool custom commit message!"
+
+ put api(url, user), params: { ids: [suggestion.id, suggestion2.id],
+ commit_message: message }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(project.repository.commit.message).to eq(message)
+ end
end
context 'when not able to apply one or more of the patches' do
diff --git a/spec/requests/api/templates_spec.rb b/spec/requests/api/templates_spec.rb
index e1c5bfd82c4..adb37c62dc3 100644
--- a/spec/requests/api/templates_spec.rb
+++ b/spec/requests/api/templates_spec.rb
@@ -65,7 +65,9 @@ RSpec.describe API::Templates do
expect(json_response['nickname']).to be_nil
expect(json_response['popular']).to be true
expect(json_response['html_url']).to eq('http://choosealicense.com/licenses/mit/')
- expect(json_response['source_url']).to eq('https://opensource.org/licenses/MIT')
+ # This was dropped:
+ # https://github.com/github/choosealicense.com/commit/325806b42aa3d5b78e84120327ec877bc936dbdd#diff-66df8f1997786f7052d29010f2cbb4c66391d60d24ca624c356acc0ab986f139
+ expect(json_response['source_url']).to be_nil
expect(json_response['description']).to include('A short and simple permissive license with conditions')
expect(json_response['conditions']).to eq(%w[include-copyright])
expect(json_response['permissions']).to eq(%w[commercial-use modifications distribution private-use])
@@ -81,7 +83,7 @@ RSpec.describe API::Templates 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.size).to eq(12)
+ expect(json_response.size).to eq(13)
expect(json_response.map { |l| l['key'] }).to include('agpl-3.0')
end
diff --git a/spec/requests/api/terraform/state_spec.rb b/spec/requests/api/terraform/state_spec.rb
index bfdb5458fd1..2cb3c8e9ab5 100644
--- a/spec/requests/api/terraform/state_spec.rb
+++ b/spec/requests/api/terraform/state_spec.rb
@@ -39,7 +39,7 @@ RSpec.describe API::Terraform::State do
context 'with maintainer permissions' do
let(:current_user) { maintainer }
- it_behaves_like 'tracking unique hll events', :usage_data_p_terraform_state_api_unique_users do
+ it_behaves_like 'tracking unique hll events' do
let(:target_id) { 'p_terraform_state_api_unique_users' }
let(:expected_type) { instance_of(Integer) }
end
diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb
index c51358bf659..55d17fabc9a 100644
--- a/spec/requests/api/triggers_spec.rb
+++ b/spec/requests/api/triggers_spec.rb
@@ -54,6 +54,15 @@ RSpec.describe API::Triggers do
expect(pipeline.builds.size).to eq(5)
end
+ it 'stores payload as a variable' do
+ post api("/projects/#{project.id}/trigger/pipeline"), params: options.merge(ref: 'master')
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(pipeline.variables.find { |v| v.key == 'TRIGGER_PAYLOAD' }.value).to eq(
+ "{\"ref\":\"master\",\"id\":\"#{project.id}\",\"variables\":{}}"
+ )
+ end
+
it 'returns bad request with no pipeline created if there\'s no commit for that ref' do
post api("/projects/#{project.id}/trigger/pipeline"), params: options.merge(ref: 'other-branch')
@@ -84,7 +93,7 @@ RSpec.describe API::Triggers do
post api("/projects/#{project.id}/trigger/pipeline"), params: options.merge(variables: variables, ref: 'master')
expect(response).to have_gitlab_http_status(:created)
- expect(pipeline.variables.map { |v| { v.key => v.value } }.last).to eq(variables)
+ expect(pipeline.variables.find { |v| v.key == 'TRIGGER_KEY' }.value).to eq('TRIGGER_VALUE')
end
end
end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 94fba451860..d70a8bd692d 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -652,6 +652,34 @@ RSpec.describe API::Users do
expect(response).to match_response_schema('public_api/v4/user/basic')
expect(json_response.keys).not_to include 'created_at'
end
+
+ it "returns the `followers` field for public users" do
+ get api("/users/#{user.id}")
+
+ expect(response).to match_response_schema('public_api/v4/user/basic')
+ expect(json_response.keys).to include 'followers'
+ end
+
+ it "does not return the `followers` field for private users" do
+ get api("/users/#{private_user.id}")
+
+ expect(response).to match_response_schema('public_api/v4/user/basic')
+ expect(json_response.keys).not_to include 'followers'
+ end
+
+ it "returns the `following` field for public users" do
+ get api("/users/#{user.id}")
+
+ expect(response).to match_response_schema('public_api/v4/user/basic')
+ expect(json_response.keys).to include 'following'
+ end
+
+ it "does not return the `following` field for private users" do
+ get api("/users/#{private_user.id}")
+
+ expect(response).to match_response_schema('public_api/v4/user/basic')
+ expect(json_response.keys).not_to include 'following'
+ end
end
it "returns a 404 error if user id not found" do
@@ -688,6 +716,128 @@ RSpec.describe API::Users do
end
end
+ describe 'POST /users/:id/follow' do
+ let(:followee) { create(:user) }
+
+ context 'on an unfollowed user' do
+ it 'follows the user' do
+ post api("/users/#{followee.id}/follow", user)
+
+ expect(user.followees).to contain_exactly(followee)
+ expect(response).to have_gitlab_http_status(:created)
+ end
+ end
+
+ context 'on a followed user' do
+ before do
+ user.follow(followee)
+ end
+
+ it 'does not change following' do
+ post api("/users/#{followee.id}/follow", user)
+
+ expect(user.followees).to contain_exactly(followee)
+ expect(response).to have_gitlab_http_status(:not_modified)
+ end
+ end
+ end
+
+ describe 'POST /users/:id/unfollow' do
+ let(:followee) { create(:user) }
+
+ context 'on a followed user' do
+ before do
+ user.follow(followee)
+ end
+
+ it 'unfollow the user' do
+ post api("/users/#{followee.id}/unfollow", user)
+
+ expect(user.followees).to be_empty
+ expect(response).to have_gitlab_http_status(:created)
+ end
+ end
+
+ context 'on an unfollowed user' do
+ it 'does not change following' do
+ post api("/users/#{followee.id}/unfollow", user)
+
+ expect(user.followees).to be_empty
+ expect(response).to have_gitlab_http_status(:not_modified)
+ end
+ end
+ end
+
+ describe 'GET /users/:id/followers' do
+ let(:follower) { create(:user) }
+
+ context 'user has followers' do
+ it 'lists followers' do
+ follower.follow(user)
+
+ get api("/users/#{user.id}/followers", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ end
+
+ it 'do not lists followers if profile is private' do
+ follower.follow(private_user)
+
+ get api("/users/#{private_user.id}/followers", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 User Not Found')
+ end
+ end
+
+ context 'user does not have any follower' do
+ it 'does list nothing' do
+ get api("/users/#{user.id}/followers", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_empty
+ end
+ end
+ end
+
+ describe 'GET /users/:id/following' do
+ let(:followee) { create(:user) }
+
+ context 'user has followers' do
+ it 'lists following user' do
+ user.follow(followee)
+
+ get api("/users/#{user.id}/following", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ end
+
+ it 'do not lists following user if profile is private' do
+ user.follow(private_user)
+
+ get api("/users/#{private_user.id}/following", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 User Not Found')
+ end
+ end
+
+ context 'user does not have any follower' do
+ it 'does list nothing' do
+ get api("/users/#{user.id}/following", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_empty
+ end
+ end
+ end
+
describe "POST /users" do
it "creates user" do
expect do
@@ -2565,7 +2715,7 @@ RSpec.describe API::Users do
it 'does not approve a deactivated user' do
expect { approve }.not_to change { deactivated_user.reload.state }
expect(response).to have_gitlab_http_status(:conflict)
- expect(json_response['message']).to eq('The user you are trying to approve is not pending an approval')
+ expect(json_response['message']).to eq('The user you are trying to approve is not pending approval')
end
end
@@ -2585,7 +2735,7 @@ RSpec.describe API::Users do
it 'returns 201' do
expect { approve }.not_to change { user.reload.state }
expect(response).to have_gitlab_http_status(:conflict)
- expect(json_response['message']).to eq('The user you are trying to approve is not pending an approval')
+ expect(json_response['message']).to eq('The user you are trying to approve is not pending approval')
end
end
@@ -2595,7 +2745,7 @@ RSpec.describe API::Users do
it 'returns 403' do
expect { approve }.not_to change { blocked_user.reload.state }
expect(response).to have_gitlab_http_status(:conflict)
- expect(json_response['message']).to eq('The user you are trying to approve is not pending an approval')
+ expect(json_response['message']).to eq('The user you are trying to approve is not pending approval')
end
end
@@ -2605,7 +2755,7 @@ RSpec.describe API::Users do
it 'returns 403' do
expect { approve }.not_to change { ldap_blocked_user.reload.state }
expect(response).to have_gitlab_http_status(:conflict)
- expect(json_response['message']).to eq('The user you are trying to approve is not pending an approval')
+ expect(json_response['message']).to eq('The user you are trying to approve is not pending approval')
end
end
@@ -2866,6 +3016,47 @@ RSpec.describe API::Users do
expect(response).to have_gitlab_http_status(:success)
expect(user.reload.status).to be_nil
end
+
+ context 'when clear_status_after is given' do
+ it 'sets the clear_status_at column' do
+ freeze_time do
+ expected_clear_status_at = 3.hours.from_now
+
+ put api('/user/status', user), params: { emoji: 'smirk', message: 'hello world', clear_status_after: '3_hours' }
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(user.status.reload.clear_status_at).to be_within(1.minute).of(expected_clear_status_at)
+ expect(Time.parse(json_response["clear_status_at"])).to be_within(1.minute).of(expected_clear_status_at)
+ end
+ end
+
+ it 'unsets the clear_status_at column' do
+ user.create_status!(clear_status_at: 5.hours.ago)
+
+ put api('/user/status', user), params: { emoji: 'smirk', message: 'hello world', clear_status_after: nil }
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(user.status.reload.clear_status_at).to be_nil
+ end
+
+ it 'raises error when unknown status value is given' do
+ put api('/user/status', user), params: { emoji: 'smirk', message: 'hello world', clear_status_after: 'wrong' }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
+ context 'when the clear_status_with_quick_options feature flag is disabled' do
+ before do
+ stub_feature_flags(clear_status_with_quick_options: false)
+ end
+
+ it 'does not persist clear_status_at' do
+ put api('/user/status', user), params: { emoji: 'smirk', message: 'hello world', clear_status_after: '3_hours' }
+
+ expect(user.status.reload.clear_status_at).to be_nil
+ end
+ end
+ end
end
describe 'POST /users/:user_id/personal_access_tokens' do
diff --git a/spec/requests/api/version_spec.rb b/spec/requests/api/version_spec.rb
index a0a0f66c8d1..7abbaf4f9ec 100644
--- a/spec/requests/api/version_spec.rb
+++ b/spec/requests/api/version_spec.rb
@@ -33,6 +33,12 @@ RSpec.describe API::Version do
expect_version
end
+
+ it 'returns "200" response on head requests' do
+ head api('/version', personal_access_token: personal_access_token)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
context 'with read_user scope' do
@@ -43,6 +49,12 @@ RSpec.describe API::Version do
expect_version
end
+
+ it 'returns "200" response on head requests' do
+ head api('/version', personal_access_token: personal_access_token)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
context 'with neither api nor read_user scope' do
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 1ee3e36be8b..a1e28c18769 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -382,6 +382,14 @@ RSpec.describe 'Git HTTP requests' do
end
end
end
+
+ context 'but the service parameter is missing' do
+ it 'rejects clones with 403 Forbidden' do
+ get("/#{path}/info/refs", headers: auth_env(*env.values_at(:user, :password), nil))
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
end
context 'and not a member of the team' do
@@ -409,6 +417,14 @@ RSpec.describe 'Git HTTP requests' do
it_behaves_like 'pushes are allowed'
end
+
+ context 'but the service parameter is missing' do
+ it 'rejects clones with 401 Unauthorized' do
+ get("/#{path}/info/refs")
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
end
end
diff --git a/spec/requests/groups/email_campaigns_controller_spec.rb b/spec/requests/groups/email_campaigns_controller_spec.rb
new file mode 100644
index 00000000000..930e645f6c0
--- /dev/null
+++ b/spec/requests/groups/email_campaigns_controller_spec.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Groups::EmailCampaignsController do
+ include InProductMarketingHelper
+ using RSpec::Parameterized::TableSyntax
+
+ describe 'GET #index', :snowplow do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:user) { create(:user) }
+ let(:track) { 'create' }
+ let(:series) { '0' }
+ let(:schema) { described_class::EMAIL_CAMPAIGNS_SCHEMA_URL }
+ let(:data) do
+ {
+ namespace_id: group.id,
+ track: track.to_sym,
+ series: series.to_i,
+ subject_line: subject_line(track.to_sym, series.to_i)
+ }
+ end
+
+ before do
+ sign_in(user)
+ group.add_developer(user)
+ allow(Gitlab::Tracking).to receive(:self_describing_event)
+ end
+
+ subject do
+ get group_email_campaigns_url(group, track: track, series: series)
+ response
+ end
+
+ shared_examples 'track and redirect' do
+ it do
+ is_expected.to track_self_describing_event(schema, data)
+ is_expected.to have_gitlab_http_status(:redirect)
+ end
+ end
+
+ shared_examples 'no track and 404' do
+ it do
+ is_expected.not_to track_self_describing_event
+ is_expected.to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ describe 'track parameter' do
+ context 'when valid' do
+ where(track: Namespaces::InProductMarketingEmailsService::TRACKS.keys)
+
+ with_them do
+ it_behaves_like 'track and redirect'
+ end
+ end
+
+ context 'when invalid' do
+ where(track: [nil, 'xxxx'])
+
+ with_them do
+ it_behaves_like 'no track and 404'
+ end
+ end
+ end
+
+ describe 'series parameter' do
+ context 'when valid' do
+ where(series: (0..Namespaces::InProductMarketingEmailsService::INTERVAL_DAYS.length - 1).to_a)
+
+ with_them do
+ it_behaves_like 'track and redirect'
+ end
+ end
+
+ context 'when invalid' do
+ where(series: [-1, nil, Namespaces::InProductMarketingEmailsService::INTERVAL_DAYS.length])
+
+ with_them do
+ it_behaves_like 'no track and 404'
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/health_controller_spec.rb b/spec/requests/health_controller_spec.rb
index 592a57bc637..f70faf5bb9c 100644
--- a/spec/requests/health_controller_spec.rb
+++ b/spec/requests/health_controller_spec.rb
@@ -77,91 +77,129 @@ RSpec.describe HealthController do
shared_context 'endpoint responding with readiness data' do
context 'when requesting instance-checks' do
- it 'responds with readiness checks data' do
- expect(Gitlab::HealthChecks::MasterCheck).to receive(:check) { true }
-
- subject
-
- expect(json_response).to include({ 'status' => 'ok' })
- expect(json_response['master_check']).to contain_exactly({ 'status' => 'ok' })
- end
+ context 'when Puma runs in Clustered mode' do
+ before do
+ allow(Gitlab::Runtime).to receive(:puma_in_clustered_mode?).and_return(true)
+ end
- it 'responds with readiness checks data when a failure happens' do
- expect(Gitlab::HealthChecks::MasterCheck).to receive(:check) { false }
+ it 'responds with readiness checks data' do
+ expect(Gitlab::HealthChecks::MasterCheck).to receive(:check) { true }
- subject
+ subject
- expect(json_response).to include({ 'status' => 'failed' })
- expect(json_response['master_check']).to contain_exactly(
- { 'status' => 'failed', 'message' => 'unexpected Master check result: false' })
+ expect(json_response).to include({ 'status' => 'ok' })
+ expect(json_response['master_check']).to contain_exactly({ 'status' => 'ok' })
+ end
- expect(response).to have_gitlab_http_status(:service_unavailable)
- expect(response.headers['X-GitLab-Custom-Error']).to eq(1)
- end
- end
+ it 'responds with readiness checks data when a failure happens' do
+ expect(Gitlab::HealthChecks::MasterCheck).to receive(:check) { false }
- context 'when requesting all checks' do
- before do
- params.merge!(all: true)
- end
+ subject
- it 'responds with readiness checks data' do
- subject
+ expect(json_response).to include({ 'status' => 'failed' })
+ expect(json_response['master_check']).to contain_exactly(
+ { 'status' => 'failed', 'message' => 'unexpected Master check result: false' })
- expect(json_response['db_check']).to contain_exactly({ 'status' => 'ok' })
- expect(json_response['cache_check']).to contain_exactly({ 'status' => 'ok' })
- expect(json_response['queues_check']).to contain_exactly({ 'status' => 'ok' })
- expect(json_response['shared_state_check']).to contain_exactly({ 'status' => 'ok' })
- expect(json_response['gitaly_check']).to contain_exactly(
- { 'status' => 'ok', 'labels' => { 'shard' => 'default' } })
+ expect(response).to have_gitlab_http_status(:service_unavailable)
+ expect(response.headers['X-GitLab-Custom-Error']).to eq(1)
+ end
end
- it 'responds with readiness checks data when a failure happens' do
- allow(Gitlab::HealthChecks::Redis::RedisCheck).to receive(:readiness).and_return(
- Gitlab::HealthChecks::Result.new('redis_check', false, "check error"))
+ context 'when Puma runs in Single mode' do
+ before do
+ allow(Gitlab::Runtime).to receive(:puma_in_clustered_mode?).and_return(false)
+ end
- subject
+ it 'does not invoke MasterCheck, succeedes' do
+ expect(Gitlab::HealthChecks::MasterCheck).not_to receive(:check) { true }
- expect(json_response['cache_check']).to contain_exactly({ 'status' => 'ok' })
- expect(json_response['redis_check']).to contain_exactly(
- { 'status' => 'failed', 'message' => 'check error' })
+ subject
- expect(response).to have_gitlab_http_status(:service_unavailable)
- expect(response.headers['X-GitLab-Custom-Error']).to eq(1)
+ expect(json_response).to eq('status' => 'ok')
+ end
end
+ end
- context 'when DB is not accessible and connection raises an exception' do
+ context 'when requesting all checks' do
+ shared_context 'endpoint responding with readiness data for all checks' do
before do
- expect(Gitlab::HealthChecks::DbCheck)
- .to receive(:readiness)
- .and_raise(PG::ConnectionBad, 'could not connect to server')
+ params.merge!(all: true)
end
- it 'responds with 500 including the exception info' do
+ it 'responds with readiness checks data' do
subject
- expect(response).to have_gitlab_http_status(:internal_server_error)
+ expect(json_response['db_check']).to contain_exactly({ 'status' => 'ok' })
+ expect(json_response['cache_check']).to contain_exactly({ 'status' => 'ok' })
+ expect(json_response['queues_check']).to contain_exactly({ 'status' => 'ok' })
+ expect(json_response['shared_state_check']).to contain_exactly({ 'status' => 'ok' })
+ expect(json_response['gitaly_check']).to contain_exactly(
+ { 'status' => 'ok', 'labels' => { 'shard' => 'default' } })
+ end
+
+ it 'responds with readiness checks data when a failure happens' do
+ allow(Gitlab::HealthChecks::Redis::RedisCheck).to receive(:readiness).and_return(
+ Gitlab::HealthChecks::Result.new('redis_check', false, "check error"))
+
+ subject
+
+ expect(json_response['cache_check']).to contain_exactly({ 'status' => 'ok' })
+ expect(json_response['redis_check']).to contain_exactly(
+ { 'status' => 'failed', 'message' => 'check error' })
+
+ expect(response).to have_gitlab_http_status(:service_unavailable)
expect(response.headers['X-GitLab-Custom-Error']).to eq(1)
- expect(json_response).to eq(
- { 'status' => 'failed', 'message' => 'PG::ConnectionBad : could not connect to server' })
+ end
+
+ context 'when DB is not accessible and connection raises an exception' do
+ before do
+ expect(Gitlab::HealthChecks::DbCheck)
+ .to receive(:readiness)
+ .and_raise(PG::ConnectionBad, 'could not connect to server')
+ end
+
+ it 'responds with 500 including the exception info' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:internal_server_error)
+ expect(response.headers['X-GitLab-Custom-Error']).to eq(1)
+ expect(json_response).to eq(
+ { 'status' => 'failed', 'message' => 'PG::ConnectionBad : could not connect to server' })
+ end
+ end
+
+ context 'when any exception happens during the probing' do
+ before do
+ expect(Gitlab::HealthChecks::Redis::RedisCheck)
+ .to receive(:readiness)
+ .and_raise(::Redis::CannotConnectError, 'Redis down')
+ end
+
+ it 'responds with 500 including the exception info' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:internal_server_error)
+ expect(response.headers['X-GitLab-Custom-Error']).to eq(1)
+ expect(json_response).to eq(
+ { 'status' => 'failed', 'message' => 'Redis::CannotConnectError : Redis down' })
+ end
end
end
- context 'when any exception happens during the probing' do
+ context 'when Puma runs in Clustered mode' do
before do
- expect(Gitlab::HealthChecks::Redis::RedisCheck)
- .to receive(:readiness)
- .and_raise(::Redis::CannotConnectError, 'Redis down')
+ allow(Gitlab::Runtime).to receive(:puma_in_clustered_mode?).and_return(true)
end
- it 'responds with 500 including the exception info' do
- subject
+ it_behaves_like 'endpoint responding with readiness data for all checks'
+ end
- expect(response).to have_gitlab_http_status(:internal_server_error)
- expect(response.headers['X-GitLab-Custom-Error']).to eq(1)
- expect(json_response).to eq(
- { 'status' => 'failed', 'message' => 'Redis::CannotConnectError : Redis down' })
+ context 'when Puma runs in Single mode' do
+ before do
+ allow(Gitlab::Runtime).to receive(:puma_in_clustered_mode?).and_return(false)
end
+
+ it_behaves_like 'endpoint responding with readiness data for all checks'
end
end
end
diff --git a/spec/requests/import/gitlab_groups_controller_spec.rb b/spec/requests/import/gitlab_groups_controller_spec.rb
index 51f1363cf1c..c65caf2ebf0 100644
--- a/spec/requests/import/gitlab_groups_controller_spec.rb
+++ b/spec/requests/import/gitlab_groups_controller_spec.rb
@@ -5,12 +5,10 @@ require 'spec_helper'
RSpec.describe Import::GitlabGroupsController do
include WorkhorseHelpers
+ include_context 'workhorse headers'
+
let_it_be(:user) { create(:user) }
let(:import_path) { "#{Dir.tmpdir}/gitlab_groups_controller_spec" }
- let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
- let(:workhorse_headers) do
- { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token }
- end
before do
allow_next_instance_of(Gitlab::ImportExport) do |import_export|
diff --git a/spec/requests/import/gitlab_projects_controller_spec.rb b/spec/requests/import/gitlab_projects_controller_spec.rb
index d7d4de21a33..58843a7fec4 100644
--- a/spec/requests/import/gitlab_projects_controller_spec.rb
+++ b/spec/requests/import/gitlab_projects_controller_spec.rb
@@ -5,8 +5,7 @@ require 'spec_helper'
RSpec.describe Import::GitlabProjectsController do
include WorkhorseHelpers
- let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
- let(:workhorse_headers) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
+ include_context 'workhorse headers'
let_it_be(:namespace) { create(:namespace) }
let_it_be(:user) { namespace.owner }
diff --git a/spec/requests/oauth/tokens_controller_spec.rb b/spec/requests/oauth/tokens_controller_spec.rb
new file mode 100644
index 00000000000..c3cdae2cd21
--- /dev/null
+++ b/spec/requests/oauth/tokens_controller_spec.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Oauth::TokensController do
+ it 'allows cross-origin POST requests' do
+ post '/oauth/token', headers: { 'Origin' => 'http://notgitlab.com' }
+
+ expect(response.headers['Access-Control-Allow-Origin']).to eq '*'
+ expect(response.headers['Access-Control-Allow-Methods']).to eq 'POST'
+ expect(response.headers['Access-Control-Allow-Headers']).to be_nil
+ expect(response.headers['Access-Control-Allow-Credentials']).to be_nil
+ end
+end
diff --git a/spec/requests/projects/ci/promeheus_metrics/histograms_controller_spec.rb b/spec/requests/projects/ci/promeheus_metrics/histograms_controller_spec.rb
new file mode 100644
index 00000000000..5d2f3e98bb4
--- /dev/null
+++ b/spec/requests/projects/ci/promeheus_metrics/histograms_controller_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Projects::Ci::PrometheusMetrics::HistogramsController' do
+ let_it_be(:project) { create(:project, :public) }
+
+ describe 'POST /*namespace_id/:project_id/-/ci/prometheus_metrics/histograms' do
+ context 'with known histograms' do
+ it 'returns 201 Created' do
+ post histograms_route(histograms: [
+ { name: :pipeline_graph_link_calculation_duration_seconds, value: 1 },
+ { name: :pipeline_graph_links_total, value: 10 }
+ ])
+
+ expect(response).to have_gitlab_http_status(:created)
+ end
+ end
+
+ context 'with unknown histograms' do
+ it 'returns 404 Not Found' do
+ post histograms_route(histograms: [{ name: :chunky_bacon, value: 5 }])
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'with the feature flag disabled' do
+ before do
+ stub_feature_flags(ci_accept_frontend_prometheus_metrics: false)
+ end
+
+ it 'returns 202 Accepted' do
+ post histograms_route(histograms: [
+ { name: :pipeline_graph_link_calculation_duration_seconds, value: 1 }
+ ])
+
+ expect(response).to have_gitlab_http_status(:accepted)
+ end
+ end
+ end
+
+ def histograms_route(params = {})
+ namespace_project_ci_prometheus_metrics_histograms_path(namespace_id: project.namespace, project_id: project, **params)
+ end
+end
diff --git a/spec/requests/projects/noteable_notes_spec.rb b/spec/requests/projects/noteable_notes_spec.rb
index 2bf1ffb2edc..5ae2aadaa84 100644
--- a/spec/requests/projects/noteable_notes_spec.rb
+++ b/spec/requests/projects/noteable_notes_spec.rb
@@ -18,9 +18,7 @@ RSpec.describe 'Project noteable notes' do
login_as(user)
end
- it 'does not set a Gitlab::EtagCaching ETag if there is a note' do
- create(:note_on_merge_request, noteable: merge_request, project: merge_request.project)
-
+ it 'does not set a Gitlab::EtagCaching ETag' do
get notes_path
expect(response).to have_gitlab_http_status(:ok)
@@ -29,12 +27,5 @@ RSpec.describe 'Project noteable notes' do
# interfere with notes pagination
expect(response_etag).not_to eq(stored_etag)
end
-
- it 'sets a Gitlab::EtagCaching ETag if there is no note' do
- get notes_path
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response_etag).to eq(stored_etag)
- end
end
end
diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb
index 34f34c0b850..1bb260b5ea1 100644
--- a/spec/requests/rack_attack_global_spec.rb
+++ b/spec/requests/rack_attack_global_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Rack Attack global throttles' do
+RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_caching do
include RackAttackSpecHelpers
let(:settings) { Gitlab::CurrentSettings.current_application_settings }
@@ -149,14 +149,14 @@ RSpec.describe 'Rack Attack global throttles' do
expect(response).to have_gitlab_http_status(:ok)
end
- arguments = {
+ arguments = a_hash_including({
message: 'Rack_Attack',
env: :throttle,
remote_ip: '127.0.0.1',
request_method: 'GET',
path: '/users/sign_in',
matched: 'throttle_unauthenticated'
- }
+ })
expect(Gitlab::AuthLogger).to receive(:error).with(arguments)
diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb
index b224ef87229..f3ddcbef1c2 100644
--- a/spec/requests/users_controller_spec.rb
+++ b/spec/requests/users_controller_spec.rb
@@ -268,6 +268,14 @@ RSpec.describe UsersController do
end
it_behaves_like 'renders all public keys'
+
+ context 'when public visibility is restricted' do
+ before do
+ stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
+ end
+
+ it_behaves_like 'renders all public keys'
+ end
end
end
end
diff --git a/spec/requests/whats_new_controller_spec.rb b/spec/requests/whats_new_controller_spec.rb
index 8005d38dbb0..ba7b5d4c000 100644
--- a/spec/requests/whats_new_controller_spec.rb
+++ b/spec/requests/whats_new_controller_spec.rb
@@ -2,64 +2,48 @@
require 'spec_helper'
-RSpec.describe WhatsNewController do
+RSpec.describe WhatsNewController, :clean_gitlab_redis_cache do
+ after do
+ ReleaseHighlight.instance_variable_set(:@file_paths, nil)
+ end
+
describe 'whats_new_path' do
let(:item) { double(:item) }
let(:highlights) { double(:highlight, items: [item], map: [item].map, next_page: 2) }
- context 'with whats_new_drawer feature enabled' do
- before do
- stub_feature_flags(whats_new_drawer: true)
- end
-
- context 'with no page param' do
- it 'responds with paginated data and headers' do
- allow(ReleaseHighlight).to receive(:paginated).with(page: 1).and_return(highlights)
- allow(Gitlab::WhatsNew::ItemPresenter).to receive(:present).with(item).and_return(item)
+ context 'with no page param' do
+ it 'responds with paginated data and headers' do
+ allow(ReleaseHighlight).to receive(:paginated).with(page: 1).and_return(highlights)
- get whats_new_path, xhr: true
+ get whats_new_path, xhr: true
- expect(response.body).to eq(highlights.items.to_json)
- expect(response.headers['X-Next-Page']).to eq(2)
- end
+ expect(response.body).to eq(highlights.items.to_json)
+ expect(response.headers['X-Next-Page']).to eq(2)
end
+ end
- context 'with page param' do
- it 'passes the page parameter' do
- expect(ReleaseHighlight).to receive(:paginated).with(page: 2).and_call_original
-
- get whats_new_path(page: 2), xhr: true
- end
-
- it 'returns a 404 if page param is negative' do
- get whats_new_path(page: -1), xhr: true
+ context 'with page param' do
+ it 'passes the page parameter' do
+ expect(ReleaseHighlight).to receive(:paginated).with(page: 2).and_call_original
- expect(response).to have_gitlab_http_status(:not_found)
- end
+ get whats_new_path(page: 2), xhr: true
end
- context 'with version param' do
- it 'returns items without pagination headers' do
- allow(ReleaseHighlight).to receive(:for_version).with(version: '42').and_return(highlights)
- allow(Gitlab::WhatsNew::ItemPresenter).to receive(:present).with(item).and_return(item)
-
- get whats_new_path(version: 42), xhr: true
+ it 'returns a 404 if page param is negative' do
+ get whats_new_path(page: -1), xhr: true
- expect(response.body).to eq(highlights.items.to_json)
- expect(response.headers['X-Next-Page']).to be_nil
- end
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
- context 'with whats_new_drawer feature disabled' do
- before do
- stub_feature_flags(whats_new_drawer: false)
- end
+ context 'with version param' do
+ it 'returns items without pagination headers' do
+ allow(ReleaseHighlight).to receive(:for_version).with(version: '42').and_return(highlights)
- it 'returns a 404' do
- get whats_new_path, xhr: true
+ get whats_new_path(version: 42), xhr: true
- expect(response).to have_gitlab_http_status(:not_found)
+ expect(response.body).to eq(highlights.items.to_json)
+ expect(response.headers['X-Next-Page']).to be_nil
end
end
end
diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb
index 9374df0c4a2..8c36d7d4668 100644
--- a/spec/routing/admin_routing_spec.rb
+++ b/spec/routing/admin_routing_spec.rb
@@ -141,13 +141,6 @@ RSpec.describe Admin::DevOpsReportController, "routing" do
end
end
-# admin_cohorts GET /admin/cohorts(.:format) admin/cohorst#index
-RSpec.describe Admin::CohortsController, "routing" do
- it "to #index" do
- expect(get("/admin/cohorts")).to route_to('admin/cohorts#index')
- end
-end
-
RSpec.describe Admin::GroupsController, "routing" do
let(:name) { 'complex.group-namegit' }
diff --git a/spec/routing/projects/security/configuration_controller_routing_spec.rb b/spec/routing/projects/security/configuration_controller_routing_spec.rb
new file mode 100644
index 00000000000..c2b10a49dea
--- /dev/null
+++ b/spec/routing/projects/security/configuration_controller_routing_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::Security::ConfigurationController, 'routing' do
+ let(:base_params) { { namespace_id: 'gitlab', project_id: 'gitlabhq' } }
+
+ before do
+ allow(Project).to receive(:find_by_full_path).with('gitlab/gitlabhq', any_args).and_return(true)
+ end
+
+ it 'routes to #show' do
+ expect(get('/gitlab/gitlabhq/-/security/configuration')).to route_to('projects/security/configuration#show', namespace_id: 'gitlab', project_id: 'gitlabhq')
+ end
+end
diff --git a/spec/rubocop/code_reuse_helpers_spec.rb b/spec/rubocop/code_reuse_helpers_spec.rb
index 574a4a85a34..4c3dd8f8167 100644
--- a/spec/rubocop/code_reuse_helpers_spec.rb
+++ b/spec/rubocop/code_reuse_helpers_spec.rb
@@ -6,7 +6,7 @@ require 'parser/current'
require_relative '../../rubocop/code_reuse_helpers'
RSpec.describe RuboCop::CodeReuseHelpers do
- def parse_source(source, path = 'foo.rb')
+ def build_and_parse_source(source, path = 'foo.rb')
buffer = Parser::Source::Buffer.new(path)
buffer.source = source
@@ -24,13 +24,13 @@ RSpec.describe RuboCop::CodeReuseHelpers do
describe '#send_to_constant?' do
it 'returns true when sending to a constant' do
- node = parse_source('Foo.bar')
+ node = build_and_parse_source('Foo.bar')
expect(cop.send_to_constant?(node)).to eq(true)
end
it 'returns false when sending to something other than a constant' do
- node = parse_source('10')
+ node = build_and_parse_source('10')
expect(cop.send_to_constant?(node)).to eq(false)
end
@@ -38,13 +38,13 @@ RSpec.describe RuboCop::CodeReuseHelpers do
describe '#send_receiver_name_ends_with?' do
it 'returns true when the receiver ends with a suffix' do
- node = parse_source('FooFinder.new')
+ node = build_and_parse_source('FooFinder.new')
expect(cop.send_receiver_name_ends_with?(node, 'Finder')).to eq(true)
end
it 'returns false when the receiver is the same as a suffix' do
- node = parse_source('Finder.new')
+ node = build_and_parse_source('Finder.new')
expect(cop.send_receiver_name_ends_with?(node, 'Finder')).to eq(false)
end
@@ -52,7 +52,7 @@ RSpec.describe RuboCop::CodeReuseHelpers do
describe '#file_path_for_node' do
it 'returns the file path of a node' do
- node = parse_source('10')
+ node = build_and_parse_source('10')
path = cop.file_path_for_node(node)
expect(path).to eq('foo.rb')
@@ -61,7 +61,7 @@ RSpec.describe RuboCop::CodeReuseHelpers do
describe '#name_of_constant' do
it 'returns the name of a constant' do
- node = parse_source('Foo')
+ node = build_and_parse_source('Foo')
expect(cop.name_of_constant(node)).to eq(:Foo)
end
@@ -69,13 +69,13 @@ RSpec.describe RuboCop::CodeReuseHelpers do
describe '#in_finder?' do
it 'returns true for a node in the finders directory' do
- node = parse_source('10', rails_root_join('app', 'finders', 'foo.rb'))
+ node = build_and_parse_source('10', rails_root_join('app', 'finders', 'foo.rb'))
expect(cop.in_finder?(node)).to eq(true)
end
it 'returns false for a node outside the finders directory' do
- node = parse_source('10', rails_root_join('app', 'foo', 'foo.rb'))
+ node = build_and_parse_source('10', rails_root_join('app', 'foo', 'foo.rb'))
expect(cop.in_finder?(node)).to eq(false)
end
@@ -83,13 +83,13 @@ RSpec.describe RuboCop::CodeReuseHelpers do
describe '#in_model?' do
it 'returns true for a node in the models directory' do
- node = parse_source('10', rails_root_join('app', 'models', 'foo.rb'))
+ node = build_and_parse_source('10', rails_root_join('app', 'models', 'foo.rb'))
expect(cop.in_model?(node)).to eq(true)
end
it 'returns false for a node outside the models directory' do
- node = parse_source('10', rails_root_join('app', 'foo', 'foo.rb'))
+ node = build_and_parse_source('10', rails_root_join('app', 'foo', 'foo.rb'))
expect(cop.in_model?(node)).to eq(false)
end
@@ -97,13 +97,13 @@ RSpec.describe RuboCop::CodeReuseHelpers do
describe '#in_service_class?' do
it 'returns true for a node in the services directory' do
- node = parse_source('10', rails_root_join('app', 'services', 'foo.rb'))
+ node = build_and_parse_source('10', rails_root_join('app', 'services', 'foo.rb'))
expect(cop.in_service_class?(node)).to eq(true)
end
it 'returns false for a node outside the services directory' do
- node = parse_source('10', rails_root_join('app', 'foo', 'foo.rb'))
+ node = build_and_parse_source('10', rails_root_join('app', 'foo', 'foo.rb'))
expect(cop.in_service_class?(node)).to eq(false)
end
@@ -111,13 +111,13 @@ RSpec.describe RuboCop::CodeReuseHelpers do
describe '#in_presenter?' do
it 'returns true for a node in the presenters directory' do
- node = parse_source('10', rails_root_join('app', 'presenters', 'foo.rb'))
+ node = build_and_parse_source('10', rails_root_join('app', 'presenters', 'foo.rb'))
expect(cop.in_presenter?(node)).to eq(true)
end
it 'returns false for a node outside the presenters directory' do
- node = parse_source('10', rails_root_join('app', 'foo', 'foo.rb'))
+ node = build_and_parse_source('10', rails_root_join('app', 'foo', 'foo.rb'))
expect(cop.in_presenter?(node)).to eq(false)
end
@@ -125,13 +125,13 @@ RSpec.describe RuboCop::CodeReuseHelpers do
describe '#in_serializer?' do
it 'returns true for a node in the serializers directory' do
- node = parse_source('10', rails_root_join('app', 'serializers', 'foo.rb'))
+ node = build_and_parse_source('10', rails_root_join('app', 'serializers', 'foo.rb'))
expect(cop.in_serializer?(node)).to eq(true)
end
it 'returns false for a node outside the serializers directory' do
- node = parse_source('10', rails_root_join('app', 'foo', 'foo.rb'))
+ node = build_and_parse_source('10', rails_root_join('app', 'foo', 'foo.rb'))
expect(cop.in_serializer?(node)).to eq(false)
end
@@ -139,13 +139,13 @@ RSpec.describe RuboCop::CodeReuseHelpers do
describe '#in_worker?' do
it 'returns true for a node in the workers directory' do
- node = parse_source('10', rails_root_join('app', 'workers', 'foo.rb'))
+ node = build_and_parse_source('10', rails_root_join('app', 'workers', 'foo.rb'))
expect(cop.in_worker?(node)).to eq(true)
end
it 'returns false for a node outside the workers directory' do
- node = parse_source('10', rails_root_join('app', 'foo', 'foo.rb'))
+ node = build_and_parse_source('10', rails_root_join('app', 'foo', 'foo.rb'))
expect(cop.in_worker?(node)).to eq(false)
end
@@ -153,13 +153,13 @@ RSpec.describe RuboCop::CodeReuseHelpers do
describe '#in_api?' do
it 'returns true for a node in the API directory' do
- node = parse_source('10', rails_root_join('lib', 'api', 'foo.rb'))
+ node = build_and_parse_source('10', rails_root_join('lib', 'api', 'foo.rb'))
expect(cop.in_api?(node)).to eq(true)
end
it 'returns false for a node outside the API directory' do
- node = parse_source('10', rails_root_join('lib', 'foo', 'foo.rb'))
+ node = build_and_parse_source('10', rails_root_join('lib', 'foo', 'foo.rb'))
expect(cop.in_api?(node)).to eq(false)
end
@@ -167,21 +167,21 @@ RSpec.describe RuboCop::CodeReuseHelpers do
describe '#in_directory?' do
it 'returns true for a directory in the CE app/ directory' do
- node = parse_source('10', rails_root_join('app', 'models', 'foo.rb'))
+ node = build_and_parse_source('10', rails_root_join('app', 'models', 'foo.rb'))
expect(cop.in_directory?(node, 'models')).to eq(true)
end
it 'returns true for a directory in the EE app/ directory' do
node =
- parse_source('10', rails_root_join('ee', 'app', 'models', 'foo.rb'))
+ build_and_parse_source('10', rails_root_join('ee', 'app', 'models', 'foo.rb'))
expect(cop.in_directory?(node, 'models')).to eq(true)
end
it 'returns false for a directory in the lib/ directory' do
node =
- parse_source('10', rails_root_join('lib', 'models', 'foo.rb'))
+ build_and_parse_source('10', rails_root_join('lib', 'models', 'foo.rb'))
expect(cop.in_directory?(node, 'models')).to eq(false)
end
@@ -189,7 +189,7 @@ RSpec.describe RuboCop::CodeReuseHelpers do
describe '#name_of_receiver' do
it 'returns the name of a send receiver' do
- node = parse_source('Foo.bar')
+ node = build_and_parse_source('Foo.bar')
expect(cop.name_of_receiver(node)).to eq('Foo')
end
@@ -197,7 +197,7 @@ RSpec.describe RuboCop::CodeReuseHelpers do
describe '#each_class_method' do
it 'yields every class method to the supplied block' do
- node = parse_source(<<~RUBY)
+ node = build_and_parse_source(<<~RUBY)
class Foo
class << self
def first
@@ -220,7 +220,7 @@ RSpec.describe RuboCop::CodeReuseHelpers do
describe '#each_send_node' do
it 'yields every send node to the supplied block' do
- node = parse_source("foo\nbar")
+ node = build_and_parse_source("foo\nbar")
nodes = cop.each_send_node(node).to_a
expect(nodes.length).to eq(2)
@@ -231,7 +231,7 @@ RSpec.describe RuboCop::CodeReuseHelpers do
describe '#disallow_send_to' do
it 'disallows sending a message to a constant' do
- def_node = parse_source(<<~RUBY)
+ def_node = build_and_parse_source(<<~RUBY)
def foo
FooFinder.new
end
diff --git a/spec/rubocop/cop/active_record_association_reload_spec.rb b/spec/rubocop/cop/active_record_association_reload_spec.rb
index 8dbe6daeeca..f28c4e60f3c 100644
--- a/spec/rubocop/cop/active_record_association_reload_spec.rb
+++ b/spec/rubocop/cop/active_record_association_reload_spec.rb
@@ -5,8 +5,6 @@ require 'rubocop'
require_relative '../../../rubocop/cop/active_record_association_reload'
RSpec.describe RuboCop::Cop::ActiveRecordAssociationReload do
- include CopHelper
-
subject(:cop) { described_class.new }
context 'when using ActiveRecord::Base' do
diff --git a/spec/rubocop/cop/api/base_spec.rb b/spec/rubocop/cop/api/base_spec.rb
index de05ab93874..ec646b9991b 100644
--- a/spec/rubocop/cop/api/base_spec.rb
+++ b/spec/rubocop/cop/api/base_spec.rb
@@ -2,12 +2,9 @@
require 'fast_spec_helper'
require 'rubocop'
-require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/api/base'
RSpec.describe RuboCop::Cop::API::Base do
- include CopHelper
-
subject(:cop) { described_class.new }
let(:corrected) do
@@ -17,7 +14,7 @@ RSpec.describe RuboCop::Cop::API::Base do
CORRECTED
end
- ['Grape::API', '::Grape::API', 'Grape::API::Instance', '::Grape::API::Instance'].each do |offense|
+ %w[Grape::API ::Grape::API Grape::API::Instance ::Grape::API::Instance].each do |offense|
it "adds an offense when inheriting from #{offense}" do
expect_offense(<<~CODE)
class SomeAPI < #{offense}
diff --git a/spec/rubocop/cop/api/grape_array_missing_coerce_spec.rb b/spec/rubocop/cop/api/grape_array_missing_coerce_spec.rb
index c7bb8255398..b50866b54b3 100644
--- a/spec/rubocop/cop/api/grape_array_missing_coerce_spec.rb
+++ b/spec/rubocop/cop/api/grape_array_missing_coerce_spec.rb
@@ -5,36 +5,38 @@ require 'rubocop'
require_relative '../../../../rubocop/cop/api/grape_array_missing_coerce'
RSpec.describe RuboCop::Cop::API::GrapeArrayMissingCoerce do
- include CopHelper
+ let(:msg) do
+ "This Grape parameter defines an Array but is missing a coerce_with definition. " \
+ "For more details, see " \
+ "https://github.com/ruby-grape/grape/blob/master/UPGRADING.md#ensure-that-array-types-have-explicit-coercions"
+ end
subject(:cop) { described_class.new }
it 'adds an offense with a required parameter' do
- inspect_source(<<~CODE)
+ expect_offense(<<~TYPE)
class SomeAPI < Grape::API::Instance
params do
requires :values, type: Array[String]
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg}
end
end
- CODE
-
- expect(cop.offenses.size).to eq(1)
+ TYPE
end
it 'adds an offense with an optional parameter' do
- inspect_source(<<~CODE)
+ expect_offense(<<~TYPE)
class SomeAPI < Grape::API::Instance
params do
optional :values, type: Array[String]
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg}
end
end
- CODE
-
- expect(cop.offenses.size).to eq(1)
+ TYPE
end
it 'does not add an offense' do
- inspect_source(<<~CODE)
+ expect_no_offenses(<<~CODE)
class SomeAPI < Grape::API::Instance
params do
requires :values, type: Array[String], coerce_with: ->(val) { val.split(',').map(&:strip) }
@@ -44,19 +46,15 @@ RSpec.describe RuboCop::Cop::API::GrapeArrayMissingCoerce do
end
end
CODE
-
- expect(cop.offenses.size).to be_zero
end
it 'does not add an offense for unrelated classes' do
- inspect_source(<<~CODE)
+ expect_no_offenses(<<~CODE)
class SomeClass
params do
requires :values, type: Array[String]
end
end
CODE
-
- expect(cop.offenses.size).to be_zero
end
end
diff --git a/spec/rubocop/cop/avoid_becomes_spec.rb b/spec/rubocop/cop/avoid_becomes_spec.rb
index 07cf374faf5..401c694f373 100644
--- a/spec/rubocop/cop/avoid_becomes_spec.rb
+++ b/spec/rubocop/cop/avoid_becomes_spec.rb
@@ -2,33 +2,31 @@
require 'fast_spec_helper'
require 'rubocop'
-require 'rubocop/rspec/support'
require_relative '../../../rubocop/cop/avoid_becomes'
RSpec.describe RuboCop::Cop::AvoidBecomes do
- include CopHelper
-
subject(:cop) { described_class.new }
it 'flags the use of becomes with a constant parameter' do
- inspect_source('foo.becomes(Project)')
-
- expect(cop.offenses.size).to eq(1)
+ expect_offense(<<~CODE)
+ foo.becomes(Project)
+ ^^^^^^^^^^^^^^^^^^^^ Avoid the use of becomes(SomeConstant), [...]
+ CODE
end
it 'flags the use of becomes with a namespaced constant parameter' do
- inspect_source('foo.becomes(Namespace::Group)')
-
- expect(cop.offenses.size).to eq(1)
+ expect_offense(<<~CODE)
+ foo.becomes(Namespace::Group)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Avoid the use of becomes(SomeConstant), [...]
+ CODE
end
it 'flags the use of becomes with a dynamic parameter' do
- inspect_source(<<~RUBY)
- model = Namespace
- project = Project.first
- project.becomes(model)
- RUBY
-
- expect(cop.offenses.size).to eq(1)
+ expect_offense(<<~CODE)
+ model = Namespace
+ project = Project.first
+ project.becomes(model)
+ ^^^^^^^^^^^^^^^^^^^^^^ Avoid the use of becomes(SomeConstant), [...]
+ CODE
end
end
diff --git a/spec/rubocop/cop/avoid_break_from_strong_memoize_spec.rb b/spec/rubocop/cop/avoid_break_from_strong_memoize_spec.rb
index 3c3aa5b7b5c..ac59d36db3f 100644
--- a/spec/rubocop/cop/avoid_break_from_strong_memoize_spec.rb
+++ b/spec/rubocop/cop/avoid_break_from_strong_memoize_spec.rb
@@ -5,8 +5,6 @@ require 'rubocop'
require_relative '../../../rubocop/cop/avoid_break_from_strong_memoize'
RSpec.describe RuboCop::Cop::AvoidBreakFromStrongMemoize do
- include CopHelper
-
subject(:cop) { described_class.new }
it 'flags violation for break inside strong_memoize' do
@@ -56,7 +54,7 @@ RSpec.describe RuboCop::Cop::AvoidBreakFromStrongMemoize do
call do
strong_memoize(:result) do
break if something
-
+ ^^^^^ Do not use break inside strong_memoize, use next instead.
do_an_heavy_calculation
end
end
@@ -65,7 +63,7 @@ RSpec.describe RuboCop::Cop::AvoidBreakFromStrongMemoize do
expect(instance).to receive(:add_offense).once
end
- inspect_source(source)
+ expect_offense(source)
end
it "doesn't check when block is empty" do
diff --git a/spec/rubocop/cop/avoid_keyword_arguments_in_sidekiq_workers_spec.rb b/spec/rubocop/cop/avoid_keyword_arguments_in_sidekiq_workers_spec.rb
index 1e1fe851840..460a0b13458 100644
--- a/spec/rubocop/cop/avoid_keyword_arguments_in_sidekiq_workers_spec.rb
+++ b/spec/rubocop/cop/avoid_keyword_arguments_in_sidekiq_workers_spec.rb
@@ -2,18 +2,15 @@
require 'fast_spec_helper'
require 'rubocop'
-require 'rubocop/rspec/support'
require_relative '../../../rubocop/cop/avoid_keyword_arguments_in_sidekiq_workers'
RSpec.describe RuboCop::Cop::AvoidKeywordArgumentsInSidekiqWorkers do
- include CopHelper
-
subject(:cop) { described_class.new }
it 'flags violation for keyword arguments usage in perform method signature' do
expect_offense(<<~RUBY)
def perform(id:)
- ^^^^^^^^^^^^^^^^ Do not use keyword arguments in Sidekiq workers. For details, check https://github.com/mperham/sidekiq/issues/2372
+ ^^^^^^^^^^^^^^^^ Do not use keyword arguments in Sidekiq workers. For details, [...]
end
RUBY
end
@@ -21,7 +18,7 @@ RSpec.describe RuboCop::Cop::AvoidKeywordArgumentsInSidekiqWorkers do
it 'flags violation for optional keyword arguments usage in perform method signature' do
expect_offense(<<~RUBY)
def perform(id: nil)
- ^^^^^^^^^^^^^^^^^^^^ Do not use keyword arguments in Sidekiq workers. For details, check https://github.com/mperham/sidekiq/issues/2372
+ ^^^^^^^^^^^^^^^^^^^^ Do not use keyword arguments in Sidekiq workers. For details, [...]
end
RUBY
end
diff --git a/spec/rubocop/cop/ban_catch_throw_spec.rb b/spec/rubocop/cop/ban_catch_throw_spec.rb
index 4f669bad4af..b3c4ad8688c 100644
--- a/spec/rubocop/cop/ban_catch_throw_spec.rb
+++ b/spec/rubocop/cop/ban_catch_throw_spec.rb
@@ -1,30 +1,28 @@
# frozen_string_literal: true
require 'fast_spec_helper'
-
require 'rubocop'
-require 'rubocop/rspec/support'
require_relative '../../../rubocop/cop/ban_catch_throw'
RSpec.describe RuboCop::Cop::BanCatchThrow do
- include CopHelper
-
subject(:cop) { described_class.new }
it 'registers an offense when `catch` or `throw` are used' do
- inspect_source("catch(:foo) {\n throw(:foo)\n}")
-
- aggregate_failures do
- expect(cop.offenses.size).to eq(2)
- expect(cop.offenses.map(&:line)).to eq([1, 2])
- expect(cop.highlights).to eq(['catch(:foo)', 'throw(:foo)'])
- end
+ expect_offense(<<~CODE)
+ catch(:foo) {
+ ^^^^^^^^^^^ Do not use catch or throw unless a gem's API demands it.
+ throw(:foo)
+ ^^^^^^^^^^^ Do not use catch or throw unless a gem's API demands it.
+ }
+ CODE
end
it 'does not register an offense for a method called catch or throw' do
- inspect_source("foo.catch(:foo) {\n foo.throw(:foo)\n}")
-
- expect(cop.offenses).to be_empty
+ expect_no_offenses(<<~CODE)
+ foo.catch(:foo) {
+ foo.throw(:foo)
+ }
+ CODE
end
end
diff --git a/spec/rubocop/cop/code_reuse/finder_spec.rb b/spec/rubocop/cop/code_reuse/finder_spec.rb
index 6f04d5e0d60..484a1549a89 100644
--- a/spec/rubocop/cop/code_reuse/finder_spec.rb
+++ b/spec/rubocop/cop/code_reuse/finder_spec.rb
@@ -2,12 +2,9 @@
require 'fast_spec_helper'
require 'rubocop'
-require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/code_reuse/finder'
RSpec.describe RuboCop::Cop::CodeReuse::Finder do
- include CopHelper
-
subject(:cop) { described_class.new }
it 'flags the use of a Finder inside another Finder' do
@@ -23,8 +20,6 @@ RSpec.describe RuboCop::Cop::CodeReuse::Finder do
end
end
SOURCE
-
- expect(cop.offenses.size).to eq(1)
end
it 'flags the use of a Finder inside a model class method' do
diff --git a/spec/rubocop/cop/code_reuse/presenter_spec.rb b/spec/rubocop/cop/code_reuse/presenter_spec.rb
index 8efd4da8aa1..4639854588e 100644
--- a/spec/rubocop/cop/code_reuse/presenter_spec.rb
+++ b/spec/rubocop/cop/code_reuse/presenter_spec.rb
@@ -2,12 +2,9 @@
require 'fast_spec_helper'
require 'rubocop'
-require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/code_reuse/presenter'
RSpec.describe RuboCop::Cop::CodeReuse::Presenter do
- include CopHelper
-
subject(:cop) { described_class.new }
it 'flags the use of a Presenter in a Service class' do
diff --git a/spec/rubocop/cop/code_reuse/serializer_spec.rb b/spec/rubocop/cop/code_reuse/serializer_spec.rb
index 74999df5859..84db2e62b41 100644
--- a/spec/rubocop/cop/code_reuse/serializer_spec.rb
+++ b/spec/rubocop/cop/code_reuse/serializer_spec.rb
@@ -2,12 +2,9 @@
require 'fast_spec_helper'
require 'rubocop'
-require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/code_reuse/serializer'
RSpec.describe RuboCop::Cop::CodeReuse::Serializer do
- include CopHelper
-
subject(:cop) { described_class.new }
it 'flags the use of a Serializer in a Service class' do
diff --git a/spec/rubocop/cop/code_reuse/service_class_spec.rb b/spec/rubocop/cop/code_reuse/service_class_spec.rb
index 4870daf72dc..b6d94dd749f 100644
--- a/spec/rubocop/cop/code_reuse/service_class_spec.rb
+++ b/spec/rubocop/cop/code_reuse/service_class_spec.rb
@@ -2,12 +2,9 @@
require 'fast_spec_helper'
require 'rubocop'
-require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/code_reuse/service_class'
RSpec.describe RuboCop::Cop::CodeReuse::ServiceClass do
- include CopHelper
-
subject(:cop) { described_class.new }
it 'flags the use of a Service class in a Finder' do
diff --git a/spec/rubocop/cop/code_reuse/worker_spec.rb b/spec/rubocop/cop/code_reuse/worker_spec.rb
index 9e015f286d8..42c9303a93b 100644
--- a/spec/rubocop/cop/code_reuse/worker_spec.rb
+++ b/spec/rubocop/cop/code_reuse/worker_spec.rb
@@ -2,12 +2,9 @@
require 'fast_spec_helper'
require 'rubocop'
-require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/code_reuse/worker'
RSpec.describe RuboCop::Cop::CodeReuse::Worker do
- include CopHelper
-
subject(:cop) { described_class.new }
it 'flags the use of a worker in a controller' do
diff --git a/spec/rubocop/cop/default_scope_spec.rb b/spec/rubocop/cop/default_scope_spec.rb
index fee1895603c..506843e030e 100644
--- a/spec/rubocop/cop/default_scope_spec.rb
+++ b/spec/rubocop/cop/default_scope_spec.rb
@@ -2,47 +2,44 @@
require 'fast_spec_helper'
require 'rubocop'
-require 'rubocop/rspec/support'
require_relative '../../../rubocop/cop/default_scope'
RSpec.describe RuboCop::Cop::DefaultScope do
- include CopHelper
-
subject(:cop) { described_class.new }
it 'does not flag the use of default_scope with a send receiver' do
- inspect_source('foo.default_scope')
-
- expect(cop.offenses.size).to eq(0)
+ expect_no_offenses('foo.default_scope')
end
it 'flags the use of default_scope with a constant receiver' do
- inspect_source('User.default_scope')
-
- expect(cop.offenses.size).to eq(1)
+ expect_offense(<<~SOURCE)
+ User.default_scope
+ ^^^^^^^^^^^^^^^^^^ Do not use `default_scope`, [...]
+ SOURCE
end
it 'flags the use of default_scope with a nil receiver' do
- inspect_source('class Foo ; default_scope ; end')
-
- expect(cop.offenses.size).to eq(1)
+ expect_offense(<<~SOURCE)
+ class Foo ; default_scope ; end
+ ^^^^^^^^^^^^^ Do not use `default_scope`, [...]
+ SOURCE
end
it 'flags the use of default_scope when passing arguments' do
- inspect_source('class Foo ; default_scope(:foo) ; end')
-
- expect(cop.offenses.size).to eq(1)
+ expect_offense(<<~SOURCE)
+ class Foo ; default_scope(:foo) ; end
+ ^^^^^^^^^^^^^^^^^^^ Do not use `default_scope`, [...]
+ SOURCE
end
it 'flags the use of default_scope when passing a block' do
- inspect_source('class Foo ; default_scope { :foo } ; end')
-
- expect(cop.offenses.size).to eq(1)
+ expect_offense(<<~SOURCE)
+ class Foo ; default_scope { :foo } ; end
+ ^^^^^^^^^^^^^ Do not use `default_scope`, [...]
+ SOURCE
end
it 'ignores the use of default_scope with a local variable receiver' do
- inspect_source('users = User.all ; users.default_scope')
-
- expect(cop.offenses.size).to eq(0)
+ expect_no_offenses('users = User.all ; users.default_scope')
end
end
diff --git a/spec/rubocop/cop/destroy_all_spec.rb b/spec/rubocop/cop/destroy_all_spec.rb
index df664724a91..f6850a00238 100644
--- a/spec/rubocop/cop/destroy_all_spec.rb
+++ b/spec/rubocop/cop/destroy_all_spec.rb
@@ -2,44 +2,41 @@
require 'fast_spec_helper'
require 'rubocop'
-require 'rubocop/rspec/support'
require_relative '../../../rubocop/cop/destroy_all'
RSpec.describe RuboCop::Cop::DestroyAll do
- include CopHelper
-
subject(:cop) { described_class.new }
it 'flags the use of destroy_all with a send receiver' do
- inspect_source('foo.destroy_all # rubocop: disable Cop/DestroyAll')
-
- expect(cop.offenses.size).to eq(1)
+ expect_offense(<<~CODE)
+ foo.destroy_all # rubocop: disable Cop/DestroyAll
+ ^^^^^^^^^^^^^^^ Use `delete_all` instead of `destroy_all`. [...]
+ CODE
end
it 'flags the use of destroy_all with a constant receiver' do
- inspect_source('User.destroy_all # rubocop: disable Cop/DestroyAll')
-
- expect(cop.offenses.size).to eq(1)
+ expect_offense(<<~CODE)
+ User.destroy_all # rubocop: disable Cop/DestroyAll
+ ^^^^^^^^^^^^^^^^ Use `delete_all` instead of `destroy_all`. [...]
+ CODE
end
it 'flags the use of destroy_all when passing arguments' do
- inspect_source('User.destroy_all([])')
-
- expect(cop.offenses.size).to eq(1)
+ expect_offense(<<~CODE)
+ User.destroy_all([])
+ ^^^^^^^^^^^^^^^^^^^^ Use `delete_all` instead of `destroy_all`. [...]
+ CODE
end
it 'flags the use of destroy_all with a local variable receiver' do
- inspect_source(<<~RUBY)
- users = User.all
- users.destroy_all # rubocop: disable Cop/DestroyAll
- RUBY
-
- expect(cop.offenses.size).to eq(1)
+ expect_offense(<<~CODE)
+ users = User.all
+ users.destroy_all # rubocop: disable Cop/DestroyAll
+ ^^^^^^^^^^^^^^^^^ Use `delete_all` instead of `destroy_all`. [...]
+ CODE
end
it 'does not flag the use of delete_all' do
- inspect_source('foo.delete_all')
-
- expect(cop.offenses).to be_empty
+ expect_no_offenses('foo.delete_all')
end
end
diff --git a/spec/rubocop/cop/gitlab/avoid_uploaded_file_from_params_spec.rb b/spec/rubocop/cop/gitlab/avoid_uploaded_file_from_params_spec.rb
index 2db03898e01..f96e25c59e7 100644
--- a/spec/rubocop/cop/gitlab/avoid_uploaded_file_from_params_spec.rb
+++ b/spec/rubocop/cop/gitlab/avoid_uploaded_file_from_params_spec.rb
@@ -2,19 +2,16 @@
require 'fast_spec_helper'
require 'rubocop'
-require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/gitlab/avoid_uploaded_file_from_params'
RSpec.describe RuboCop::Cop::Gitlab::AvoidUploadedFileFromParams do
- include CopHelper
-
subject(:cop) { described_class.new }
- context 'UploadedFile.from_params' do
+ context 'when using UploadedFile.from_params' do
it 'flags its call' do
expect_offense(<<~SOURCE)
- UploadedFile.from_params(params)
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use the `UploadedFile` set by `multipart.rb` instead of calling `UploadedFile.from_params` directly. See https://docs.gitlab.com/ee/development/uploads.html#how-to-add-a-new-upload-route
+ UploadedFile.from_params(params)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use the `UploadedFile` set by `multipart.rb` instead of calling [...]
SOURCE
end
end
diff --git a/spec/rubocop/cop/gitlab/bulk_insert_spec.rb b/spec/rubocop/cop/gitlab/bulk_insert_spec.rb
index ad7e685e505..c280ab8fa8b 100644
--- a/spec/rubocop/cop/gitlab/bulk_insert_spec.rb
+++ b/spec/rubocop/cop/gitlab/bulk_insert_spec.rb
@@ -2,25 +2,22 @@
require 'fast_spec_helper'
require 'rubocop'
-require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/gitlab/bulk_insert'
RSpec.describe RuboCop::Cop::Gitlab::BulkInsert do
- include CopHelper
-
subject(:cop) { described_class.new }
it 'flags the use of Gitlab::Database.bulk_insert' do
expect_offense(<<~SOURCE)
- Gitlab::Database.bulk_insert('merge_request_diff_files', rows)
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{RuboCop::Cop::Gitlab::BulkInsert::MSG}
+ Gitlab::Database.bulk_insert('merge_request_diff_files', rows)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use the `BulkInsertSafe` concern, [...]
SOURCE
end
it 'flags the use of ::Gitlab::Database.bulk_insert' do
expect_offense(<<~SOURCE)
- ::Gitlab::Database.bulk_insert('merge_request_diff_files', rows)
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{RuboCop::Cop::Gitlab::BulkInsert::MSG}
+ ::Gitlab::Database.bulk_insert('merge_request_diff_files', rows)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use the `BulkInsertSafe` concern, [...]
SOURCE
end
end
diff --git a/spec/rubocop/cop/gitlab/change_timezone_spec.rb b/spec/rubocop/cop/gitlab/change_timezone_spec.rb
index 6abbc06bb1a..9cb822ec4f2 100644
--- a/spec/rubocop/cop/gitlab/change_timezone_spec.rb
+++ b/spec/rubocop/cop/gitlab/change_timezone_spec.rb
@@ -2,19 +2,16 @@
require 'fast_spec_helper'
require 'rubocop'
-require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/gitlab/change_timzone'
RSpec.describe RuboCop::Cop::Gitlab::ChangeTimezone do
- include CopHelper
-
subject(:cop) { described_class.new }
context 'Time.zone=' do
it 'registers an offense with no 2nd argument' do
expect_offense(<<~PATTERN)
Time.zone = 'Awkland'
- ^^^^^^^^^^^^^^^^^^^^^ Do not change timezone in the runtime (application or rspec), it could result in silently modifying other behavior.
+ ^^^^^^^^^^^^^^^^^^^^^ Do not change timezone in the runtime (application or rspec), it could result [...]
PATTERN
end
end
diff --git a/spec/rubocop/cop/gitlab/const_get_inherit_false_spec.rb b/spec/rubocop/cop/gitlab/const_get_inherit_false_spec.rb
index bed06ab2b17..19e5fe946be 100644
--- a/spec/rubocop/cop/gitlab/const_get_inherit_false_spec.rb
+++ b/spec/rubocop/cop/gitlab/const_get_inherit_false_spec.rb
@@ -2,78 +2,75 @@
require 'fast_spec_helper'
require 'rubocop'
-require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/gitlab/const_get_inherit_false'
RSpec.describe RuboCop::Cop::Gitlab::ConstGetInheritFalse do
- include CopHelper
-
subject(:cop) { described_class.new }
context 'Object.const_get' do
- it 'registers an offense with no 2nd argument' do
+ it 'registers an offense with no 2nd argument and corrects' do
expect_offense(<<~PATTERN)
Object.const_get(:CONSTANT)
^^^^^^^^^ Use inherit=false when using const_get.
PATTERN
- end
- it 'autocorrects' do
- expect(autocorrect_source('Object.const_get(:CONSTANT)')).to eq('Object.const_get(:CONSTANT, false)')
+ expect_correction(<<~PATTERN)
+ Object.const_get(:CONSTANT, false)
+ PATTERN
end
context 'inherit=false' do
it 'does not register an offense' do
expect_no_offenses(<<~PATTERN)
- Object.const_get(:CONSTANT, false)
+ Object.const_get(:CONSTANT, false)
PATTERN
end
end
context 'inherit=true' do
- it 'registers an offense' do
+ it 'registers an offense and corrects' do
expect_offense(<<~PATTERN)
- Object.const_get(:CONSTANT, true)
- ^^^^^^^^^ Use inherit=false when using const_get.
+ Object.const_get(:CONSTANT, true)
+ ^^^^^^^^^ Use inherit=false when using const_get.
PATTERN
- end
- it 'autocorrects' do
- expect(autocorrect_source('Object.const_get(:CONSTANT, true)')).to eq('Object.const_get(:CONSTANT, false)')
+ expect_correction(<<~PATTERN)
+ Object.const_get(:CONSTANT, false)
+ PATTERN
end
end
end
context 'const_get for a nested class' do
- it 'registers an offense on reload usage' do
+ it 'registers an offense on reload usage and corrects' do
expect_offense(<<~PATTERN)
Nested::Blog.const_get(:CONSTANT)
^^^^^^^^^ Use inherit=false when using const_get.
PATTERN
- end
- it 'autocorrects' do
- expect(autocorrect_source('Nested::Blag.const_get(:CONSTANT)')).to eq('Nested::Blag.const_get(:CONSTANT, false)')
+ expect_correction(<<~PATTERN)
+ Nested::Blog.const_get(:CONSTANT, false)
+ PATTERN
end
context 'inherit=false' do
it 'does not register an offense' do
expect_no_offenses(<<~PATTERN)
- Nested::Blog.const_get(:CONSTANT, false)
+ Nested::Blog.const_get(:CONSTANT, false)
PATTERN
end
end
context 'inherit=true' do
- it 'registers an offense if inherit is true' do
+ it 'registers an offense if inherit is true and corrects' do
expect_offense(<<~PATTERN)
- Nested::Blog.const_get(:CONSTANT, true)
- ^^^^^^^^^ Use inherit=false when using const_get.
+ Nested::Blog.const_get(:CONSTANT, true)
+ ^^^^^^^^^ Use inherit=false when using const_get.
PATTERN
- end
- it 'autocorrects' do
- expect(autocorrect_source('Nested::Blag.const_get(:CONSTANT, true)')).to eq('Nested::Blag.const_get(:CONSTANT, false)')
+ expect_correction(<<~PATTERN)
+ Nested::Blog.const_get(:CONSTANT, false)
+ PATTERN
end
end
end
diff --git a/spec/rubocop/cop/gitlab/duplicate_spec_location_spec.rb b/spec/rubocop/cop/gitlab/duplicate_spec_location_spec.rb
index 5804b03b641..a207155f432 100644
--- a/spec/rubocop/cop/gitlab/duplicate_spec_location_spec.rb
+++ b/spec/rubocop/cop/gitlab/duplicate_spec_location_spec.rb
@@ -6,8 +6,6 @@ require 'rubocop'
require_relative '../../../../rubocop/cop/gitlab/duplicate_spec_location'
RSpec.describe RuboCop::Cop::Gitlab::DuplicateSpecLocation do
- include CopHelper
-
subject(:cop) { described_class.new }
let(:rails_root) { '../../../../' }
diff --git a/spec/rubocop/cop/gitlab/except_spec.rb b/spec/rubocop/cop/gitlab/except_spec.rb
index 173e5943da5..7a122e3cf53 100644
--- a/spec/rubocop/cop/gitlab/except_spec.rb
+++ b/spec/rubocop/cop/gitlab/except_spec.rb
@@ -2,12 +2,9 @@
require 'fast_spec_helper'
require 'rubocop'
-require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/gitlab/except'
RSpec.describe RuboCop::Cop::Gitlab::Except do
- include CopHelper
-
subject(:cop) { described_class.new }
it 'flags the use of Gitlab::SQL::Except.new' do
diff --git a/spec/rubocop/cop/gitlab/finder_with_find_by_spec.rb b/spec/rubocop/cop/gitlab/finder_with_find_by_spec.rb
index db3bcf1dfdb..03d7fc5e8b1 100644
--- a/spec/rubocop/cop/gitlab/finder_with_find_by_spec.rb
+++ b/spec/rubocop/cop/gitlab/finder_with_find_by_spec.rb
@@ -1,46 +1,31 @@
# frozen_string_literal: true
require 'fast_spec_helper'
-
require 'rubocop'
-require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/gitlab/finder_with_find_by'
RSpec.describe RuboCop::Cop::Gitlab::FinderWithFindBy do
- include CopHelper
-
subject(:cop) { described_class.new }
context 'when calling execute.find' do
- let(:source) do
- <<~SRC
- DummyFinder.new(some_args)
- .execute
- .find_by!(1)
- SRC
- end
-
- let(:corrected_source) do
- <<~SRC
- DummyFinder.new(some_args)
- .find_by!(1)
- SRC
- end
-
- it 'registers an offence' do
- inspect_source(source)
-
- expect(cop.offenses.size).to eq(1)
- end
-
- it 'can autocorrect the source' do
- expect(autocorrect_source(source)).to eq(corrected_source)
+ it 'registers an offense and corrects' do
+ expect_offense(<<~CODE)
+ DummyFinder.new(some_args)
+ .execute
+ .find_by!(1)
+ ^^^^^^^^ Don't chain finders `#execute` method with [...]
+ CODE
+
+ expect_correction(<<~CODE)
+ DummyFinder.new(some_args)
+ .find_by!(1)
+ CODE
end
context 'when called within the `FinderMethods` module' do
- let(:source) do
- <<~SRC
+ it 'does not register an offense' do
+ expect_no_offenses(<<~SRC)
module FinderMethods
def find_by!(*args)
execute.find_by!(args)
@@ -48,12 +33,6 @@ RSpec.describe RuboCop::Cop::Gitlab::FinderWithFindBy do
end
SRC
end
-
- it 'does not register an offence' do
- inspect_source(source)
-
- expect(cop.offenses).to be_empty
- end
end
end
end
diff --git a/spec/rubocop/cop/gitlab/httparty_spec.rb b/spec/rubocop/cop/gitlab/httparty_spec.rb
index b112ac84bff..fcd18b0eb9b 100644
--- a/spec/rubocop/cop/gitlab/httparty_spec.rb
+++ b/spec/rubocop/cop/gitlab/httparty_spec.rb
@@ -2,46 +2,30 @@
require 'fast_spec_helper'
require 'rubocop'
-require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/gitlab/httparty'
RSpec.describe RuboCop::Cop::Gitlab::HTTParty do # rubocop:disable RSpec/FilePath
- include CopHelper
-
subject(:cop) { described_class.new }
- shared_examples('registering include offense') do |options|
- let(:offending_lines) { options[:offending_lines] }
-
+ shared_examples('registering include offense') do
it 'registers an offense when the class includes HTTParty' do
- inspect_source(source)
-
- aggregate_failures do
- expect(cop.offenses.size).to eq(offending_lines.size)
- expect(cop.offenses.map(&:line)).to eq(offending_lines)
- end
+ expect_offense(source)
end
end
- shared_examples('registering call offense') do |options|
- let(:offending_lines) { options[:offending_lines] }
-
+ shared_examples('registering call offense') do
it 'registers an offense when the class calls HTTParty' do
- inspect_source(source)
-
- aggregate_failures do
- expect(cop.offenses.size).to eq(offending_lines.size)
- expect(cop.offenses.map(&:line)).to eq(offending_lines)
- end
+ expect_offense(source)
end
end
context 'when source is a regular module' do
- it_behaves_like 'registering include offense', offending_lines: [2] do
+ it_behaves_like 'registering include offense' do
let(:source) do
<<~RUBY
module M
include HTTParty
+ ^^^^^^^^^^^^^^^^ Avoid including `HTTParty` directly. [...]
end
RUBY
end
@@ -49,11 +33,12 @@ RSpec.describe RuboCop::Cop::Gitlab::HTTParty do # rubocop:disable RSpec/FilePat
end
context 'when source is a regular class' do
- it_behaves_like 'registering include offense', offending_lines: [2] do
+ it_behaves_like 'registering include offense' do
let(:source) do
<<~RUBY
class Foo
include HTTParty
+ ^^^^^^^^^^^^^^^^ Avoid including `HTTParty` directly. [...]
end
RUBY
end
@@ -61,12 +46,13 @@ RSpec.describe RuboCop::Cop::Gitlab::HTTParty do # rubocop:disable RSpec/FilePat
end
context 'when HTTParty is called' do
- it_behaves_like 'registering call offense', offending_lines: [3] do
+ it_behaves_like 'registering call offense' do
let(:source) do
<<~RUBY
class Foo
def bar
HTTParty.get('http://example.com')
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Avoid calling `HTTParty` directly. [...]
end
end
RUBY
diff --git a/spec/rubocop/cop/gitlab/intersect_spec.rb b/spec/rubocop/cop/gitlab/intersect_spec.rb
index e724f47029c..6f0367591cd 100644
--- a/spec/rubocop/cop/gitlab/intersect_spec.rb
+++ b/spec/rubocop/cop/gitlab/intersect_spec.rb
@@ -2,12 +2,9 @@
require 'fast_spec_helper'
require 'rubocop'
-require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/gitlab/intersect'
RSpec.describe RuboCop::Cop::Gitlab::Intersect do
- include CopHelper
-
subject(:cop) { described_class.new }
it 'flags the use of Gitlab::SQL::Intersect.new' do
diff --git a/spec/rubocop/cop/gitlab/json_spec.rb b/spec/rubocop/cop/gitlab/json_spec.rb
index fc25f69a244..29c3b96cc1a 100644
--- a/spec/rubocop/cop/gitlab/json_spec.rb
+++ b/spec/rubocop/cop/gitlab/json_spec.rb
@@ -2,38 +2,21 @@
require 'fast_spec_helper'
require 'rubocop'
-require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/gitlab/json'
RSpec.describe RuboCop::Cop::Gitlab::Json do
- include CopHelper
-
subject(:cop) { described_class.new }
- shared_examples('registering call offense') do |options|
- let(:offending_lines) { options[:offending_lines] }
-
- it 'registers an offense when the class calls JSON' do
- inspect_source(source)
-
- aggregate_failures do
- expect(cop.offenses.size).to eq(offending_lines.size)
- expect(cop.offenses.map(&:line)).to eq(offending_lines)
- end
- end
- end
-
context 'when JSON is called' do
- it_behaves_like 'registering call offense', offending_lines: [3] do
- let(:source) do
- <<~RUBY
- class Foo
- def bar
- JSON.parse('{ "foo": "bar" }')
- end
+ it 'registers an offense' do
+ expect_offense(<<~RUBY)
+ class Foo
+ def bar
+ JSON.parse('{ "foo": "bar" }')
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Avoid calling `JSON` directly. [...]
end
- RUBY
- end
+ end
+ RUBY
end
end
end
diff --git a/spec/rubocop/cop/gitlab/module_with_instance_variables_spec.rb b/spec/rubocop/cop/gitlab/module_with_instance_variables_spec.rb
index 1d09c720bf7..08634d5753a 100644
--- a/spec/rubocop/cop/gitlab/module_with_instance_variables_spec.rb
+++ b/spec/rubocop/cop/gitlab/module_with_instance_variables_spec.rb
@@ -2,42 +2,33 @@
require 'fast_spec_helper'
require 'rubocop'
-require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/gitlab/module_with_instance_variables'
RSpec.describe RuboCop::Cop::Gitlab::ModuleWithInstanceVariables do
- include CopHelper
+ let(:msg) { "Do not use instance variables in a module. [...]" }
subject(:cop) { described_class.new }
- shared_examples('registering offense') do |options|
- let(:offending_lines) { options[:offending_lines] }
-
+ shared_examples('registering offense') do
it 'registers an offense when instance variable is used in a module' do
- inspect_source(source)
-
- aggregate_failures do
- expect(cop.offenses.size).to eq(offending_lines.size)
- expect(cop.offenses.map(&:line)).to eq(offending_lines)
- end
+ expect_offense(source)
end
end
shared_examples('not registering offense') do
it 'does not register offenses' do
- inspect_source(source)
-
- expect(cop.offenses).to be_empty
+ expect_no_offenses(source)
end
end
context 'when source is a regular module' do
- it_behaves_like 'registering offense', offending_lines: [3] do
+ it_behaves_like 'registering offense' do
let(:source) do
<<~RUBY
module M
def f
@f = true
+ ^^^^^^^^^ #{msg}
end
end
RUBY
@@ -46,13 +37,14 @@ RSpec.describe RuboCop::Cop::Gitlab::ModuleWithInstanceVariables do
end
context 'when source is a nested module' do
- it_behaves_like 'registering offense', offending_lines: [4] do
+ it_behaves_like 'registering offense' do
let(:source) do
<<~RUBY
module N
module M
def f
@f = true
+ ^^^^^^^^^ #{msg}
end
end
end
@@ -62,13 +54,14 @@ RSpec.describe RuboCop::Cop::Gitlab::ModuleWithInstanceVariables do
end
context 'when source is a nested module with multiple offenses' do
- it_behaves_like 'registering offense', offending_lines: [4, 12] do
+ it_behaves_like 'registering offense' do
let(:source) do
<<~RUBY
module N
module M
def f
@f = true
+ ^^^^^^^^^ #{msg}
end
def g
@@ -77,6 +70,7 @@ RSpec.describe RuboCop::Cop::Gitlab::ModuleWithInstanceVariables do
def h
@h = true
+ ^^^^^^^^^ #{msg}
end
end
end
@@ -129,12 +123,13 @@ RSpec.describe RuboCop::Cop::Gitlab::ModuleWithInstanceVariables do
end
context 'when source is using simple or ivar assignment with other ivar' do
- it_behaves_like 'registering offense', offending_lines: [3] do
+ it_behaves_like 'registering offense' do
let(:source) do
<<~RUBY
module M
def f
@f ||= g(@g)
+ ^^ #{msg}
end
end
RUBY
@@ -143,13 +138,15 @@ RSpec.describe RuboCop::Cop::Gitlab::ModuleWithInstanceVariables do
end
context 'when source is using or ivar assignment with something else' do
- it_behaves_like 'registering offense', offending_lines: [3, 4] do
+ it_behaves_like 'registering offense' do
let(:source) do
<<~RUBY
module M
def f
@f ||= true
+ ^^ #{msg}
@f.to_s
+ ^^ #{msg}
end
end
RUBY
diff --git a/spec/rubocop/cop/gitlab/namespaced_class_spec.rb b/spec/rubocop/cop/gitlab/namespaced_class_spec.rb
new file mode 100644
index 00000000000..d1f61aa5afb
--- /dev/null
+++ b/spec/rubocop/cop/gitlab/namespaced_class_spec.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'rubocop'
+require 'rubocop/rspec/support'
+require_relative '../../../../rubocop/cop/gitlab/namespaced_class'
+
+RSpec.describe RuboCop::Cop::Gitlab::NamespacedClass do
+ subject(:cop) { described_class.new }
+
+ it 'flags a class definition without namespace' do
+ expect_offense(<<~SOURCE)
+ class MyClass
+ ^^^^^^^^^^^^^ #{described_class::MSG}
+ end
+ SOURCE
+ end
+
+ it 'flags a class definition with inheritance without namespace' do
+ expect_offense(<<~SOURCE)
+ class MyClass < ApplicationRecord
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{described_class::MSG}
+ def some_method
+ true
+ end
+ end
+ SOURCE
+ end
+
+ it 'does not flag the class definition with namespace in separate lines' do
+ expect_no_offenses(<<~SOURCE)
+ module MyModule
+ class MyClass < ApplicationRecord
+ end
+
+ class MyOtherClass
+ def other_method
+ 1 + 1
+ end
+ end
+ end
+ SOURCE
+ end
+
+ it 'does not flag the class definition with nested namespace in separate lines' do
+ expect_no_offenses(<<~SOURCE)
+ module TopLevelModule
+ module NestedModule
+ class MyClass
+ end
+ end
+ end
+ SOURCE
+ end
+
+ it 'does not flag the class definition nested inside namespaced class' do
+ expect_no_offenses(<<~SOURCE)
+ module TopLevelModule
+ class TopLevelClass
+ class MyClass
+ end
+ end
+ end
+ SOURCE
+ end
+
+ it 'does not flag a compact namespaced class definition' do
+ expect_no_offenses(<<~SOURCE)
+ class MyModule::MyClass < ApplicationRecord
+ end
+ SOURCE
+ end
+end
diff --git a/spec/rubocop/cop/gitlab/policy_rule_boolean_spec.rb b/spec/rubocop/cop/gitlab/policy_rule_boolean_spec.rb
index e6fb9ab9d57..6dbbcdd8324 100644
--- a/spec/rubocop/cop/gitlab/policy_rule_boolean_spec.rb
+++ b/spec/rubocop/cop/gitlab/policy_rule_boolean_spec.rb
@@ -2,12 +2,9 @@
require 'fast_spec_helper'
require 'rubocop'
-require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/gitlab/policy_rule_boolean'
RSpec.describe RuboCop::Cop::Gitlab::PolicyRuleBoolean do
- include CopHelper
-
subject(:cop) { described_class.new }
it 'registers offense for &&' do
diff --git a/spec/rubocop/cop/gitlab/predicate_memoization_spec.rb b/spec/rubocop/cop/gitlab/predicate_memoization_spec.rb
index 322c7c82968..071ddcf8b7d 100644
--- a/spec/rubocop/cop/gitlab/predicate_memoization_spec.rb
+++ b/spec/rubocop/cop/gitlab/predicate_memoization_spec.rb
@@ -2,55 +2,38 @@
require 'fast_spec_helper'
require 'rubocop'
-require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/gitlab/predicate_memoization'
RSpec.describe RuboCop::Cop::Gitlab::PredicateMemoization do
- include CopHelper
-
subject(:cop) { described_class.new }
- shared_examples('registering offense') do |options|
- let(:offending_lines) { options[:offending_lines] }
-
- it 'registers an offense when a predicate method is memoizing via ivar' do
- inspect_source(source)
-
- aggregate_failures do
- expect(cop.offenses.size).to eq(offending_lines.size)
- expect(cop.offenses.map(&:line)).to eq(offending_lines)
- end
- end
- end
-
shared_examples('not registering offense') do
it 'does not register offenses' do
- inspect_source(source)
-
- expect(cop.offenses).to be_empty
+ expect_no_offenses(source)
end
end
- context 'when source is a predicate method memoizing via ivar' do
- it_behaves_like 'registering offense', offending_lines: [3] do
+ context 'when source is a predicate method using ivar with assignment' do
+ it_behaves_like 'not registering offense' do
let(:source) do
<<~RUBY
class C
def really?
- @really ||= true
+ @really = true
end
end
RUBY
end
end
+ end
- it_behaves_like 'registering offense', offending_lines: [4] do
+ context 'when source is a predicate method using local with ||=' do
+ it_behaves_like 'not registering offense' do
let(:source) do
<<~RUBY
class C
def really?
- value = true
- @really ||= value
+ really ||= true
end
end
RUBY
@@ -58,13 +41,13 @@ RSpec.describe RuboCop::Cop::Gitlab::PredicateMemoization do
end
end
- context 'when source is a predicate method using ivar with assignment' do
+ context 'when source is a regular method memoizing via ivar' do
it_behaves_like 'not registering offense' do
let(:source) do
<<~RUBY
class C
- def really?
- @really = true
+ def really
+ @really ||= true
end
end
RUBY
@@ -72,30 +55,37 @@ RSpec.describe RuboCop::Cop::Gitlab::PredicateMemoization do
end
end
- context 'when source is a predicate method using local with ||=' do
- it_behaves_like 'not registering offense' do
- let(:source) do
- <<~RUBY
+ context 'when source is a predicate method memoizing via ivar' do
+ let(:msg) { "Avoid using `@value ||= query` [...]" }
+
+ context 'when assigning to boolean' do
+ it 'registers an offense' do
+ node = "@really ||= true"
+
+ expect_offense(<<~CODE, node: node, msg: msg)
class C
def really?
- really ||= true
+ %{node}
+ ^{node} %{msg}
end
end
- RUBY
+ CODE
end
end
- end
- context 'when source is a regular method memoizing via ivar' do
- it_behaves_like 'not registering offense' do
- let(:source) do
- <<~RUBY
+ context 'when assigning to another variable that is a boolean' do
+ it 'registers an offense' do
+ node = "@really ||= value"
+
+ expect_offense(<<~CODE, node: node, msg: msg)
class C
- def really
- @really ||= true
+ def really?
+ value = true
+ %{node}
+ ^{node} %{msg}
end
end
- RUBY
+ CODE
end
end
end
diff --git a/spec/rubocop/cop/gitlab/rails_logger_spec.rb b/spec/rubocop/cop/gitlab/rails_logger_spec.rb
index 768da243b02..7258b047191 100644
--- a/spec/rubocop/cop/gitlab/rails_logger_spec.rb
+++ b/spec/rubocop/cop/gitlab/rails_logger_spec.rb
@@ -2,37 +2,31 @@
require 'fast_spec_helper'
require 'rubocop'
-require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/gitlab/rails_logger'
RSpec.describe RuboCop::Cop::Gitlab::RailsLogger do
- include CopHelper
-
subject(:cop) { described_class.new }
described_class::LOG_METHODS.each do |method|
it "flags the use of Rails.logger.#{method} with a constant receiver" do
- inspect_source("Rails.logger.#{method}('some error')")
+ node = "Rails.logger.#{method}('some error')"
- expect(cop.offenses.size).to eq(1)
+ expect_offense(<<~CODE, node: node, msg: "Use a structured JSON logger instead of `Rails.logger`. [...]")
+ %{node}
+ ^{node} %{msg}
+ CODE
end
end
it 'does not flag the use of Rails.logger with a constant that is not Rails' do
- inspect_source("AppLogger.error('some error')")
-
- expect(cop.offenses.size).to eq(0)
+ expect_no_offenses("AppLogger.error('some error')")
end
it 'does not flag the use of logger with a send receiver' do
- inspect_source("file_logger.info('important info')")
-
- expect(cop.offenses.size).to eq(0)
+ expect_no_offenses("file_logger.info('important info')")
end
it 'does not flag the use of Rails.logger.level' do
- inspect_source("Rails.logger.level")
-
- expect(cop.offenses.size).to eq(0)
+ expect_no_offenses("Rails.logger.level")
end
end
diff --git a/spec/rubocop/cop/gitlab/union_spec.rb b/spec/rubocop/cop/gitlab/union_spec.rb
index 20364b1b901..04a3db8e7dd 100644
--- a/spec/rubocop/cop/gitlab/union_spec.rb
+++ b/spec/rubocop/cop/gitlab/union_spec.rb
@@ -2,12 +2,9 @@
require 'fast_spec_helper'
require 'rubocop'
-require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/gitlab/union'
RSpec.describe RuboCop::Cop::Gitlab::Union do
- include CopHelper
-
subject(:cop) { described_class.new }
it 'flags the use of Gitlab::SQL::Union.new' do
diff --git a/spec/rubocop/cop/graphql/authorize_types_spec.rb b/spec/rubocop/cop/graphql/authorize_types_spec.rb
index a1b7a3f3a9b..9242b865b20 100644
--- a/spec/rubocop/cop/graphql/authorize_types_spec.rb
+++ b/spec/rubocop/cop/graphql/authorize_types_spec.rb
@@ -6,21 +6,18 @@ require 'rubocop'
require_relative '../../../../rubocop/cop/graphql/authorize_types'
RSpec.describe RuboCop::Cop::Graphql::AuthorizeTypes do
- include CopHelper
-
subject(:cop) { described_class.new }
it 'adds an offense when there is no authorize call' do
- inspect_source(<<~TYPE)
+ expect_offense(<<~TYPE)
module Types
class AType < BaseObject
+ ^^^^^^^^^^^^^^^^^^^^^^^^ Add an `authorize :ability` call to the type: https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#type-authorization
field :a_thing
field :another_thing
end
end
TYPE
-
- expect(cop.offenses.size).to eq 1
end
it 'does not add an offense for classes that have an authorize call' do
diff --git a/spec/rubocop/cop/graphql/descriptions_spec.rb b/spec/rubocop/cop/graphql/descriptions_spec.rb
index b44205b0920..9ad40fad83d 100644
--- a/spec/rubocop/cop/graphql/descriptions_spec.rb
+++ b/spec/rubocop/cop/graphql/descriptions_spec.rb
@@ -5,38 +5,34 @@ require 'rubocop'
require_relative '../../../../rubocop/cop/graphql/descriptions'
RSpec.describe RuboCop::Cop::Graphql::Descriptions do
- include CopHelper
-
subject(:cop) { described_class.new }
context 'fields' do
it 'adds an offense when there is no description' do
- inspect_source(<<~TYPE)
+ expect_offense(<<~TYPE)
module Types
class FakeType < BaseObject
field :a_thing,
+ ^^^^^^^^^^^^^^^ Please add a `description` property.
GraphQL::STRING_TYPE,
null: false
end
end
TYPE
-
- expect(cop.offenses.size).to eq 1
end
it 'adds an offense when description does not end in a period' do
- inspect_source(<<~TYPE)
+ expect_offense(<<~TYPE)
module Types
class FakeType < BaseObject
field :a_thing,
+ ^^^^^^^^^^^^^^^ `description` strings must end with a `.`.
GraphQL::STRING_TYPE,
null: false,
description: 'A descriptive description'
end
end
TYPE
-
- expect(cop.offenses.size).to eq 1
end
it 'does not add an offense when description is correct' do
@@ -55,32 +51,30 @@ RSpec.describe RuboCop::Cop::Graphql::Descriptions do
context 'arguments' do
it 'adds an offense when there is no description' do
- inspect_source(<<~TYPE)
+ expect_offense(<<~TYPE)
module Types
class FakeType < BaseObject
argument :a_thing,
+ ^^^^^^^^^^^^^^^^^^ Please add a `description` property.
GraphQL::STRING_TYPE,
null: false
end
end
TYPE
-
- expect(cop.offenses.size).to eq 1
end
it 'adds an offense when description does not end in a period' do
- inspect_source(<<~TYPE)
+ expect_offense(<<~TYPE)
module Types
class FakeType < BaseObject
argument :a_thing,
+ ^^^^^^^^^^^^^^^^^^ `description` strings must end with a `.`.
GraphQL::STRING_TYPE,
null: false,
description: 'Behold! A description'
end
end
TYPE
-
- expect(cop.offenses.size).to eq 1
end
it 'does not add an offense when description is correct' do
diff --git a/spec/rubocop/cop/graphql/gid_expected_type_spec.rb b/spec/rubocop/cop/graphql/gid_expected_type_spec.rb
index 8fd7ae03748..d9a129244d6 100644
--- a/spec/rubocop/cop/graphql/gid_expected_type_spec.rb
+++ b/spec/rubocop/cop/graphql/gid_expected_type_spec.rb
@@ -6,16 +6,13 @@ require 'rubocop'
require_relative '../../../../rubocop/cop/graphql/gid_expected_type'
RSpec.describe RuboCop::Cop::Graphql::GIDExpectedType do
- include CopHelper
-
subject(:cop) { described_class.new }
it 'adds an offense when there is no expected_type parameter' do
- inspect_source(<<~TYPE)
+ expect_offense(<<~TYPE)
GitlabSchema.object_from_id(received_id)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Add an expected_type parameter to #object_from_id calls if possible.
TYPE
-
- expect(cop.offenses.size).to eq 1
end
it 'does not add an offense for calls that have an expected_type parameter' do
diff --git a/spec/rubocop/cop/graphql/id_type_spec.rb b/spec/rubocop/cop/graphql/id_type_spec.rb
index 6135c9fef43..93c01cd7f06 100644
--- a/spec/rubocop/cop/graphql/id_type_spec.rb
+++ b/spec/rubocop/cop/graphql/id_type_spec.rb
@@ -6,16 +6,13 @@ require 'rubocop'
require_relative '../../../../rubocop/cop/graphql/id_type'
RSpec.describe RuboCop::Cop::Graphql::IDType do
- include CopHelper
-
subject(:cop) { described_class.new }
it 'adds an offense when GraphQL::ID_TYPE is used as a param to #argument' do
- inspect_source(<<~TYPE)
+ expect_offense(<<~TYPE)
argument :some_arg, GraphQL::ID_TYPE, some: other, params: do_not_matter
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Do not use GraphQL::ID_TYPE, use a specific GlobalIDType instead
TYPE
-
- expect(cop.offenses.size).to eq 1
end
context 'whitelisted arguments' do
diff --git a/spec/rubocop/cop/graphql/json_type_spec.rb b/spec/rubocop/cop/graphql/json_type_spec.rb
index 6d9f86e44d2..91838c1708e 100644
--- a/spec/rubocop/cop/graphql/json_type_spec.rb
+++ b/spec/rubocop/cop/graphql/json_type_spec.rb
@@ -5,29 +5,29 @@ require 'rubocop'
require_relative '../../../../rubocop/cop/graphql/json_type'
RSpec.describe RuboCop::Cop::Graphql::JSONType do
- include CopHelper
+ let(:msg) do
+ 'Avoid using GraphQL::Types::JSON. See: https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#json'
+ end
subject(:cop) { described_class.new }
context 'fields' do
it 'adds an offense when GraphQL::Types::JSON is used' do
- inspect_source(<<~RUBY.strip)
+ expect_offense(<<~RUBY)
class MyType
field :some_field, GraphQL::Types::JSON
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg}
end
RUBY
-
- expect(cop.offenses.size).to eq(1)
end
it 'adds an offense when GraphQL::Types::JSON is used with other keywords' do
- inspect_source(<<~RUBY.strip)
+ expect_offense(<<~RUBY)
class MyType
field :some_field, GraphQL::Types::JSON, null: true, description: 'My description'
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg}
end
RUBY
-
- expect(cop.offenses.size).to eq(1)
end
it 'does not add an offense for other types' do
@@ -41,23 +41,21 @@ RSpec.describe RuboCop::Cop::Graphql::JSONType do
context 'arguments' do
it 'adds an offense when GraphQL::Types::JSON is used' do
- inspect_source(<<~RUBY.strip)
+ expect_offense(<<~RUBY)
class MyType
argument :some_arg, GraphQL::Types::JSON
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg}
end
RUBY
-
- expect(cop.offenses.size).to eq(1)
end
it 'adds an offense when GraphQL::Types::JSON is used with other keywords' do
- inspect_source(<<~RUBY.strip)
+ expect_offense(<<~RUBY)
class MyType
argument :some_arg, GraphQL::Types::JSON, null: true, description: 'My description'
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg}
end
RUBY
-
- expect(cop.offenses.size).to eq(1)
end
it 'does not add an offense for other types' do
diff --git a/spec/rubocop/cop/graphql/resolver_type_spec.rb b/spec/rubocop/cop/graphql/resolver_type_spec.rb
index 25213e30528..11c0ad284a9 100644
--- a/spec/rubocop/cop/graphql/resolver_type_spec.rb
+++ b/spec/rubocop/cop/graphql/resolver_type_spec.rb
@@ -6,24 +6,19 @@ require 'rubocop'
require_relative '../../../../rubocop/cop/graphql/resolver_type'
RSpec.describe RuboCop::Cop::Graphql::ResolverType do
- include CopHelper
-
subject(:cop) { described_class.new }
- it 'adds an offense when there is no type annotaion' do
- lacks_type = <<-SRC
+ it 'adds an offense when there is no type annotation' do
+ expect_offense(<<~SRC)
module Resolvers
class FooResolver < BaseResolver
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Missing type annotation: Please add `type` DSL method call. e.g: type UserType.connection_type, null: true
def resolve(**args)
[:thing]
end
end
end
SRC
-
- inspect_source(lacks_type)
-
- expect(cop.offenses.size).to eq 1
end
it 'does not add an offense for resolvers that have a type call' do
@@ -41,9 +36,10 @@ RSpec.describe RuboCop::Cop::Graphql::ResolverType do
end
it 'ignores type calls on other objects' do
- lacks_type = <<-SRC
+ expect_offense(<<~SRC)
module Resolvers
class FooResolver < BaseResolver
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Missing type annotation: Please add `type` DSL method call. e.g: type UserType.connection_type, null: true
class FalsePositive < BaseObject
type RedHerringType, null: true
end
@@ -54,10 +50,6 @@ RSpec.describe RuboCop::Cop::Graphql::ResolverType do
end
end
SRC
-
- inspect_source(lacks_type)
-
- expect(cop.offenses.size).to eq 1
end
it 'does not add an offense unless the class is named using the Resolver convention' do
diff --git a/spec/rubocop/cop/group_public_or_visible_to_user_spec.rb b/spec/rubocop/cop/group_public_or_visible_to_user_spec.rb
index ac6c481a7c3..b3ec426dc07 100644
--- a/spec/rubocop/cop/group_public_or_visible_to_user_spec.rb
+++ b/spec/rubocop/cop/group_public_or_visible_to_user_spec.rb
@@ -2,29 +2,28 @@
require 'fast_spec_helper'
require 'rubocop'
-require 'rubocop/rspec/support'
require_relative '../../../rubocop/cop/group_public_or_visible_to_user'
RSpec.describe RuboCop::Cop::GroupPublicOrVisibleToUser do
- include CopHelper
+ let(:msg) do
+ "`Group.public_or_visible_to_user` should be used with extreme care. " \
+ "Please ensure that you are not using it on its own and that the amount of rows being filtered is reasonable."
+ end
subject(:cop) { described_class.new }
it 'flags the use of Group.public_or_visible_to_user with a constant receiver' do
- inspect_source('Group.public_or_visible_to_user')
-
- expect(cop.offenses.size).to eq(1)
+ expect_offense(<<~CODE)
+ Group.public_or_visible_to_user
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg}
+ CODE
end
- it 'does not flat the use of public_or_visible_to_user with a constant that is not Group' do
- inspect_source('Project.public_or_visible_to_user')
-
- expect(cop.offenses.size).to eq(0)
+ it 'does not flag the use of public_or_visible_to_user with a constant that is not Group' do
+ expect_no_offenses('Project.public_or_visible_to_user')
end
it 'does not flag the use of Group.public_or_visible_to_user with a send receiver' do
- inspect_source('foo.public_or_visible_to_user')
-
- expect(cop.offenses.size).to eq(0)
+ expect_no_offenses('foo.public_or_visible_to_user')
end
end
diff --git a/spec/rubocop/cop/include_sidekiq_worker_spec.rb b/spec/rubocop/cop/include_sidekiq_worker_spec.rb
index f12652a1a58..bdd622d4894 100644
--- a/spec/rubocop/cop/include_sidekiq_worker_spec.rb
+++ b/spec/rubocop/cop/include_sidekiq_worker_spec.rb
@@ -3,33 +3,21 @@
require 'fast_spec_helper'
require 'rubocop'
-require 'rubocop/rspec/support'
-
require_relative '../../../rubocop/cop/include_sidekiq_worker'
RSpec.describe RuboCop::Cop::IncludeSidekiqWorker do
- include CopHelper
-
subject(:cop) { described_class.new }
context 'when `Sidekiq::Worker` is included' do
- let(:source) { 'include Sidekiq::Worker' }
- let(:correct_source) { 'include ApplicationWorker' }
-
- it 'registers an offense' do
- inspect_source(source)
-
- aggregate_failures do
- expect(cop.offenses.size).to eq(1)
- expect(cop.offenses.map(&:line)).to eq([1])
- expect(cop.highlights).to eq(['Sidekiq::Worker'])
- end
- end
-
- it 'autocorrects to the right version' do
- autocorrected = autocorrect_source(source)
-
- expect(autocorrected).to eq(correct_source)
+ it 'registers an offense and corrects', :aggregate_failures do
+ expect_offense(<<~CODE)
+ include Sidekiq::Worker
+ ^^^^^^^^^^^^^^^ Include `ApplicationWorker`, not `Sidekiq::Worker`.
+ CODE
+
+ expect_correction(<<~CODE)
+ include ApplicationWorker
+ CODE
end
end
end
diff --git a/spec/rubocop/cop/inject_enterprise_edition_module_spec.rb b/spec/rubocop/cop/inject_enterprise_edition_module_spec.rb
index 47247006e42..2d293fd0a05 100644
--- a/spec/rubocop/cop/inject_enterprise_edition_module_spec.rb
+++ b/spec/rubocop/cop/inject_enterprise_edition_module_spec.rb
@@ -2,12 +2,9 @@
require 'fast_spec_helper'
require 'rubocop'
-require 'rubocop/rspec/support'
require_relative '../../../rubocop/cop/inject_enterprise_edition_module'
RSpec.describe RuboCop::Cop::InjectEnterpriseEditionModule do
- include CopHelper
-
subject(:cop) { described_class.new }
it 'flags the use of `prepend_if_ee EE` in the middle of a file' do
@@ -185,18 +182,19 @@ RSpec.describe RuboCop::Cop::InjectEnterpriseEditionModule do
end
it 'autocorrects offenses by just disabling the Cop' do
- source = <<~SOURCE
- class Foo
- prepend_if_ee 'EE::Foo'
- include_if_ee 'Bar'
- end
+ expect_offense(<<~SOURCE)
+ class Foo
+ prepend_if_ee 'EE::Foo'
+ ^^^^^^^^^^^^^^^^^^^^^^^ Injecting EE modules must be done on the last line of this file, outside of any class or module definitions
+ include_if_ee 'Bar'
+ end
SOURCE
- expect(autocorrect_source(source)).to eq(<<~SOURCE)
- class Foo
- prepend_if_ee 'EE::Foo' # rubocop: disable Cop/InjectEnterpriseEditionModule
- include_if_ee 'Bar'
- end
+ expect_correction(<<~SOURCE)
+ class Foo
+ prepend_if_ee 'EE::Foo' # rubocop: disable Cop/InjectEnterpriseEditionModule
+ include_if_ee 'Bar'
+ end
SOURCE
end
diff --git a/spec/rubocop/cop/lint/last_keyword_argument_spec.rb b/spec/rubocop/cop/lint/last_keyword_argument_spec.rb
index 826c681a880..aac59f0db4c 100644
--- a/spec/rubocop/cop/lint/last_keyword_argument_spec.rb
+++ b/spec/rubocop/cop/lint/last_keyword_argument_spec.rb
@@ -5,8 +5,6 @@ require 'rubocop'
require_relative '../../../../rubocop/cop/lint/last_keyword_argument'
RSpec.describe RuboCop::Cop::Lint::LastKeywordArgument do
- include CopHelper
-
subject(:cop) { described_class.new }
before do
diff --git a/spec/rubocop/cop/migration/add_limit_to_text_columns_spec.rb b/spec/rubocop/cop/migration/add_limit_to_text_columns_spec.rb
index 97b9d0d1ee2..149fb0a48eb 100644
--- a/spec/rubocop/cop/migration/add_limit_to_text_columns_spec.rb
+++ b/spec/rubocop/cop/migration/add_limit_to_text_columns_spec.rb
@@ -28,6 +28,15 @@ RSpec.describe RuboCop::Cop::Migration::AddLimitToTextColumns do
^^^^ #{described_class::MSG}
end
+ create_table_with_constraints :test_text_limits_create do |t|
+ t.integer :test_id, null: false
+ t.text :title
+ t.text :description
+ ^^^^ #{described_class::MSG}
+
+ t.text_limit :title, 100
+ end
+
add_column :test_text_limits, :email, :text
^^^^^^^^^^ #{described_class::MSG}
@@ -57,6 +66,15 @@ RSpec.describe RuboCop::Cop::Migration::AddLimitToTextColumns do
t.text :name
end
+ create_table_with_constraints :test_text_limits_create do |t|
+ t.integer :test_id, null: false
+ t.text :title
+ t.text :description
+
+ t.text_limit :title, 100
+ t.text_limit :description, 255
+ end
+
add_column :test_text_limits, :email, :text
add_column_with_default :test_text_limits, :role, :text, default: 'default'
change_column_type_concurrently :test_text_limits, :test_id, :text
diff --git a/spec/rubocop/cop/performance/ar_count_each_spec.rb b/spec/rubocop/cop/performance/ar_count_each_spec.rb
index 6242c7a4c5e..402e3e93147 100644
--- a/spec/rubocop/cop/performance/ar_count_each_spec.rb
+++ b/spec/rubocop/cop/performance/ar_count_each_spec.rb
@@ -5,8 +5,6 @@ require 'rubocop'
require_relative '../../../../rubocop/cop/performance/ar_count_each.rb'
RSpec.describe RuboCop::Cop::Performance::ARCountEach do
- include CopHelper
-
subject(:cop) { described_class.new }
context 'when it is not haml file' do
@@ -32,8 +30,6 @@ RSpec.describe RuboCop::Cop::Performance::ARCountEach do
^^^^^^^^^^^^ If @users is AR relation, avoid `@users.count ...; @users.each... `, this will trigger two queries. Use `@users.load.size ...; @users.each... ` instead. If @users is an array, try to use @users.size.
@users.each { |user| display(user) }
SOURCE
-
- expect(cop.offenses.map(&:cop_name)).to contain_exactly('Performance/ARCountEach')
end
end
diff --git a/spec/rubocop/cop/performance/ar_exists_and_present_blank_spec.rb b/spec/rubocop/cop/performance/ar_exists_and_present_blank_spec.rb
index 3321d400ae1..8497ff0e909 100644
--- a/spec/rubocop/cop/performance/ar_exists_and_present_blank_spec.rb
+++ b/spec/rubocop/cop/performance/ar_exists_and_present_blank_spec.rb
@@ -5,8 +5,6 @@ require 'rubocop'
require_relative '../../../../rubocop/cop/performance/ar_exists_and_present_blank.rb'
RSpec.describe RuboCop::Cop::Performance::ARExistsAndPresentBlank do
- include CopHelper
-
subject(:cop) { described_class.new }
context 'when it is not haml file' do
@@ -32,8 +30,6 @@ RSpec.describe RuboCop::Cop::Performance::ARExistsAndPresentBlank do
show @users if @users.present?
^^^^^^^^^^^^^^^ Avoid `@users.present?`, because it will generate database query 'Select TABLE.*' which is expensive. Suggest to use `@users.any?` to replace `@users.present?`
SOURCE
-
- expect(cop.offenses.map(&:cop_name)).to contain_exactly('Performance/ARExistsAndPresentBlank')
end
end
@@ -44,8 +40,6 @@ RSpec.describe RuboCop::Cop::Performance::ARExistsAndPresentBlank do
show @users if @users.blank?
^^^^^^^^^^^^^ Avoid `@users.blank?`, because it will generate database query 'Select TABLE.*' which is expensive. Suggest to use `@users.empty?` to replace `@users.blank?`
SOURCE
-
- expect(cop.offenses.map(&:cop_name)).to contain_exactly('Performance/ARExistsAndPresentBlank')
end
end
@@ -58,8 +52,6 @@ RSpec.describe RuboCop::Cop::Performance::ARExistsAndPresentBlank do
show @users if @users.present?
^^^^^^^^^^^^^^^ Avoid `@users.present?`, because it will generate database query 'Select TABLE.*' which is expensive. Suggest to use `@users.any?` to replace `@users.present?`
SOURCE
-
- expect(cop.offenses.map(&:cop_name)).to contain_exactly('Performance/ARExistsAndPresentBlank', 'Performance/ARExistsAndPresentBlank')
end
end
diff --git a/spec/rubocop/cop/performance/readlines_each_spec.rb b/spec/rubocop/cop/performance/readlines_each_spec.rb
index c19426606f6..5a30107722a 100644
--- a/spec/rubocop/cop/performance/readlines_each_spec.rb
+++ b/spec/rubocop/cop/performance/readlines_each_spec.rb
@@ -5,19 +5,21 @@ require 'rubocop'
require_relative '../../../../rubocop/cop/performance/readlines_each'
RSpec.describe RuboCop::Cop::Performance::ReadlinesEach do
- include CopHelper
-
subject(:cop) { described_class.new }
let(:message) { 'Avoid `IO.readlines.each`, since it reads contents into memory in full. Use `IO.each_line` or `IO.each` instead.' }
shared_examples_for(:class_read) do |klass|
context "and it is called as a class method on #{klass}" do
- # We can't use `expect_offense` here because indentation changes based on `klass`
it 'flags it as an offense' do
- inspect_source "#{klass}.readlines(file_path).each { |line| puts line }"
+ leading_readline = "#{klass}.readlines(file_path)."
+ padding = " " * leading_readline.length
+ node = "#{leading_readline}each { |line| puts line }"
- expect(cop.offenses.map(&:cop_name)).to contain_exactly('Performance/ReadlinesEach')
+ expect_offense(<<~CODE, node: node)
+ %{node}
+ #{padding}^^^^ Avoid `IO.readlines.each`, since it reads contents into memory in full. [...]
+ CODE
end
end
@@ -62,9 +64,14 @@ RSpec.describe RuboCop::Cop::Performance::ReadlinesEach do
end
it 'autocorrects `readlines.each` to `each_line`' do
- expect(autocorrect_source('obj.readlines.each { |line| line }')).to(
- eq('obj.each_line { |line| line }')
- )
+ expect_offense(<<~CODE)
+ obj.readlines.each { |line| line }
+ ^^^^ Avoid `IO.readlines.each`, since it reads contents into memory in full. [...]
+ CODE
+
+ expect_correction(<<~CODE)
+ obj.each_line { |line| line }
+ CODE
end
end
diff --git a/spec/rubocop/cop/project_path_helper_spec.rb b/spec/rubocop/cop/project_path_helper_spec.rb
index 78a590b89f6..16782802a27 100644
--- a/spec/rubocop/cop/project_path_helper_spec.rb
+++ b/spec/rubocop/cop/project_path_helper_spec.rb
@@ -3,41 +3,30 @@
require 'fast_spec_helper'
require 'rubocop'
-require 'rubocop/rspec/support'
-
require_relative '../../../rubocop/cop/project_path_helper'
RSpec.describe RuboCop::Cop::ProjectPathHelper do
- include CopHelper
-
subject(:cop) { described_class.new }
context "when using namespace_project with the project's namespace" do
let(:source) { 'edit_namespace_project_issue_path(@issue.project.namespace, @issue.project, @issue)' }
let(:correct_source) { 'edit_project_issue_path(@issue.project, @issue)' }
- it 'registers an offense' do
- inspect_source(source)
-
- aggregate_failures do
- expect(cop.offenses.size).to eq(1)
- expect(cop.offenses.map(&:line)).to eq([1])
- expect(cop.highlights).to eq(['edit_namespace_project_issue_path'])
- end
- end
-
- it 'autocorrects to the right version' do
- autocorrected = autocorrect_source(source)
+ it 'registers an offense and corrects', :aggregate_failures do
+ expect_offense(<<~CODE)
+ #{source}
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use short project path helpers without explicitly passing the namespace[...]
+ CODE
- expect(autocorrected).to eq(correct_source)
+ expect_correction(<<~CODE)
+ #{correct_source}
+ CODE
end
end
context 'when using namespace_project with a different namespace' do
it 'registers no offense' do
- inspect_source('edit_namespace_project_issue_path(namespace, project)')
-
- expect(cop.offenses.size).to eq(0)
+ expect_no_offenses('edit_namespace_project_issue_path(namespace, project)')
end
end
end
diff --git a/spec/rubocop/cop/put_project_routes_under_scope_spec.rb b/spec/rubocop/cop/put_project_routes_under_scope_spec.rb
index b0627af0e8b..eb783d22129 100644
--- a/spec/rubocop/cop/put_project_routes_under_scope_spec.rb
+++ b/spec/rubocop/cop/put_project_routes_under_scope_spec.rb
@@ -5,8 +5,6 @@ require 'rubocop'
require_relative '../../../rubocop/cop/put_project_routes_under_scope'
RSpec.describe RuboCop::Cop::PutProjectRoutesUnderScope do
- include CopHelper
-
subject(:cop) { described_class.new }
%w[resource resources get post put patch delete].each do |route_method|
diff --git a/spec/rubocop/cop/qa/ambiguous_page_object_name_spec.rb b/spec/rubocop/cop/qa/ambiguous_page_object_name_spec.rb
index 4876fcd5050..9332ab4186e 100644
--- a/spec/rubocop/cop/qa/ambiguous_page_object_name_spec.rb
+++ b/spec/rubocop/cop/qa/ambiguous_page_object_name_spec.rb
@@ -1,15 +1,11 @@
# frozen_string_literal: true
require 'fast_spec_helper'
-
require 'rubocop'
-require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/qa/ambiguous_page_object_name'
RSpec.describe RuboCop::Cop::QA::AmbiguousPageObjectName do
- include CopHelper
-
let(:source_file) { 'qa/page.rb' }
subject(:cop) { described_class.new }
diff --git a/spec/rubocop/cop/qa/element_with_pattern_spec.rb b/spec/rubocop/cop/qa/element_with_pattern_spec.rb
index 6289b1a7c97..28c351ccf1e 100644
--- a/spec/rubocop/cop/qa/element_with_pattern_spec.rb
+++ b/spec/rubocop/cop/qa/element_with_pattern_spec.rb
@@ -1,15 +1,11 @@
# frozen_string_literal: true
require 'fast_spec_helper'
-
require 'rubocop'
-require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/qa/element_with_pattern'
RSpec.describe RuboCop::Cop::QA::ElementWithPattern do
- include CopHelper
-
let(:source_file) { 'qa/page.rb' }
subject(:cop) { described_class.new }
diff --git a/spec/rubocop/cop/rspec/any_instance_of_spec.rb b/spec/rubocop/cop/rspec/any_instance_of_spec.rb
index 42bb7d196a1..e7675ded25e 100644
--- a/spec/rubocop/cop/rspec/any_instance_of_spec.rb
+++ b/spec/rubocop/cop/rspec/any_instance_of_spec.rb
@@ -5,59 +5,51 @@ require 'fast_spec_helper'
require_relative '../../../../rubocop/cop/rspec/any_instance_of'
RSpec.describe RuboCop::Cop::RSpec::AnyInstanceOf do
- include CopHelper
-
subject(:cop) { described_class.new }
context 'when calling allow_any_instance_of' do
let(:source) do
<<~SRC
- allow_any_instance_of(User).to receive(:invalidate_issue_cache_counts)
+ allow_any_instance_of(User).to receive(:invalidate_issue_cache_counts)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Do not use `allow_any_instance_of` [...]
SRC
end
let(:corrected_source) do
<<~SRC
- allow_next_instance_of(User) do |instance|
- allow(instance).to receive(:invalidate_issue_cache_counts)
- end
+ allow_next_instance_of(User) do |instance|
+ allow(instance).to receive(:invalidate_issue_cache_counts)
+ end
SRC
end
- it 'registers an offence' do
- inspect_source(source)
-
- expect(cop.offenses.size).to eq(1)
- end
+ it 'registers an offense and corrects', :aggregate_failures do
+ expect_offense(source)
- it 'can autocorrect the source' do
- expect(autocorrect_source(source)).to eq(corrected_source)
+ expect_correction(corrected_source)
end
end
context 'when calling expect_any_instance_of' do
let(:source) do
<<~SRC
- expect_any_instance_of(User).to receive(:invalidate_issue_cache_counts).with(args).and_return(double)
+ expect_any_instance_of(User).to receive(:invalidate_issue_cache_counts).with(args).and_return(double)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Do not use `expect_any_instance_of` [...]
SRC
end
let(:corrected_source) do
<<~SRC
- expect_next_instance_of(User) do |instance|
- expect(instance).to receive(:invalidate_issue_cache_counts).with(args).and_return(double)
- end
+ expect_next_instance_of(User) do |instance|
+ expect(instance).to receive(:invalidate_issue_cache_counts).with(args).and_return(double)
+ end
SRC
end
- it 'registers an offence' do
- inspect_source(source)
-
- expect(cop.offenses.size).to eq(1)
- end
+ it 'registers an offense and corrects', :aggregate_failures do
+ expect_offense(source)
- it 'can autocorrect the source' do
- expect(autocorrect_source(source)).to eq(corrected_source)
+ expect_correction(corrected_source)
end
end
end
diff --git a/spec/rubocop/cop/rspec/be_success_matcher_spec.rb b/spec/rubocop/cop/rspec/be_success_matcher_spec.rb
index d49507c89b1..050f0396fac 100644
--- a/spec/rubocop/cop/rspec/be_success_matcher_spec.rb
+++ b/spec/rubocop/cop/rspec/be_success_matcher_spec.rb
@@ -5,34 +5,27 @@ require 'rubocop'
require_relative '../../../../rubocop/cop/rspec/be_success_matcher'
RSpec.describe RuboCop::Cop::RSpec::BeSuccessMatcher do
- include CopHelper
-
let(:source_file) { 'spec/foo_spec.rb' }
subject(:cop) { described_class.new }
shared_examples 'cop' do |good:, bad:|
context "using #{bad} call" do
- it 'registers an offense' do
- inspect_source(bad, source_file)
-
- expect(cop.offenses.size).to eq(1)
- expect(cop.offenses.map(&:line)).to eq([1])
- expect(cop.highlights).to eq([bad])
- end
-
- it "autocorrects it to `#{good}`" do
- autocorrected = autocorrect_source(bad, source_file)
-
- expect(autocorrected).to eql(good)
+ it 'registers an offense and corrects', :aggregate_failures do
+ expect_offense(<<~CODE, node: bad)
+ %{node}
+ ^{node} Do not use deprecated `success?` method, use `successful?` instead.
+ CODE
+
+ expect_correction(<<~CODE)
+ #{good}
+ CODE
end
end
context "using #{good} call" do
it 'does not register an offense' do
- inspect_source(good)
-
- expect(cop.offenses).to be_empty
+ expect_no_offenses(good)
end
end
end
diff --git a/spec/rubocop/cop/rspec/env_assignment_spec.rb b/spec/rubocop/cop/rspec/env_assignment_spec.rb
index 07afd30fc90..cc132d1532a 100644
--- a/spec/rubocop/cop/rspec/env_assignment_spec.rb
+++ b/spec/rubocop/cop/rspec/env_assignment_spec.rb
@@ -3,13 +3,9 @@
require 'fast_spec_helper'
require 'rubocop'
-require 'rubocop/rspec/support'
-
require_relative '../../../../rubocop/cop/rspec/env_assignment'
RSpec.describe RuboCop::Cop::RSpec::EnvAssignment do
- include CopHelper
-
offense_call_single_quotes_key = %(ENV['FOO'] = 'bar').freeze
offense_call_double_quotes_key = %(ENV["FOO"] = 'bar').freeze
@@ -17,31 +13,24 @@ RSpec.describe RuboCop::Cop::RSpec::EnvAssignment do
subject(:cop) { described_class.new }
- shared_examples 'an offensive ENV#[]= call' do |content|
- it "registers an offense for `#{content}`" do
- inspect_source(content, source_file)
-
- expect(cop.offenses.size).to eq(1)
- expect(cop.offenses.map(&:line)).to eq([1])
- expect(cop.highlights).to eq([content])
- end
- end
-
- shared_examples 'an autocorrected ENV#[]= call' do |content, autocorrected_content|
- it "registers an offense for `#{content}` and autocorrects it to `#{autocorrected_content}`" do
- autocorrected = autocorrect_source(content, source_file)
+ shared_examples 'an offensive and correction ENV#[]= call' do |content, autocorrected_content|
+ it "registers an offense for `#{content}` and corrects", :aggregate_failures do
+ expect_offense(<<~CODE)
+ #{content}
+ ^^^^^^^^^^^^^^^^^^ Don't assign to ENV, use `stub_env` instead.
+ CODE
- expect(autocorrected).to eql(autocorrected_content)
+ expect_correction(<<~CODE)
+ #{autocorrected_content}
+ CODE
end
end
context 'with a key using single quotes' do
- it_behaves_like 'an offensive ENV#[]= call', offense_call_single_quotes_key
- it_behaves_like 'an autocorrected ENV#[]= call', offense_call_single_quotes_key, %(stub_env('FOO', 'bar'))
+ it_behaves_like 'an offensive and correction ENV#[]= call', offense_call_single_quotes_key, %(stub_env('FOO', 'bar'))
end
context 'with a key using double quotes' do
- it_behaves_like 'an offensive ENV#[]= call', offense_call_double_quotes_key
- it_behaves_like 'an autocorrected ENV#[]= call', offense_call_double_quotes_key, %(stub_env("FOO", 'bar'))
+ it_behaves_like 'an offensive and correction ENV#[]= call', offense_call_double_quotes_key, %(stub_env("FOO", 'bar'))
end
end
diff --git a/spec/rubocop/cop/rspec/expect_gitlab_tracking_spec.rb b/spec/rubocop/cop/rspec/expect_gitlab_tracking_spec.rb
index f7adc1373df..d1ce8d01e0b 100644
--- a/spec/rubocop/cop/rspec/expect_gitlab_tracking_spec.rb
+++ b/spec/rubocop/cop/rspec/expect_gitlab_tracking_spec.rb
@@ -2,13 +2,9 @@
require 'fast_spec_helper'
require 'rubocop'
-require 'rubocop/rspec/support'
-
require_relative '../../../../rubocop/cop/rspec/expect_gitlab_tracking'
RSpec.describe RuboCop::Cop::RSpec::ExpectGitlabTracking do
- include CopHelper
-
let(:source_file) { 'spec/foo_spec.rb' }
subject(:cop) { described_class.new }
@@ -36,29 +32,18 @@ RSpec.describe RuboCop::Cop::RSpec::ExpectGitlabTracking do
good_samples.each do |good|
context "good: #{good}" do
it 'does not register an offense' do
- inspect_source(good)
-
- expect(cop.offenses).to be_empty
+ expect_no_offenses(good)
end
end
end
bad_samples.each do |bad|
context "bad: #{bad}" do
- it 'registers an offense', :aggregate_failures do
- inspect_source(bad, source_file)
-
- expect(cop.offenses.size).to eq(1)
- expect(cop.offenses.map(&:line)).to eq([1])
- expect(cop.highlights).to eq([bad])
-
- msg = cop.offenses.first.message
-
- expect(msg).to match(
- /Do not expect directly on `Gitlab::Tracking#event`/
- )
- expect(msg).to match(/add the `snowplow` annotation/)
- expect(msg).to match(/use `expect_snowplow_event` instead/)
+ it 'registers an offense' do
+ expect_offense(<<~CODE, node: bad)
+ %{node}
+ ^{node} Do not expect directly on `Gitlab::Tracking#event`[...]
+ CODE
end
end
end
diff --git a/spec/rubocop/cop/rspec/factories_in_migration_specs_spec.rb b/spec/rubocop/cop/rspec/factories_in_migration_specs_spec.rb
index fe9cea47a43..8beec53375e 100644
--- a/spec/rubocop/cop/rspec/factories_in_migration_specs_spec.rb
+++ b/spec/rubocop/cop/rspec/factories_in_migration_specs_spec.rb
@@ -3,17 +3,13 @@
require 'fast_spec_helper'
require 'rubocop'
-require 'rubocop/rspec/support'
-
require_relative '../../../../rubocop/cop/rspec/factories_in_migration_specs'
RSpec.describe RuboCop::Cop::RSpec::FactoriesInMigrationSpecs do
- include CopHelper
-
subject(:cop) { described_class.new }
shared_examples 'an offensive factory call' do |namespace|
- %i[build build_list create create_list].each do |forbidden_method|
+ %i[build build_list create create_list attributes_for].each do |forbidden_method|
namespaced_forbidden_method = "#{namespace}#{forbidden_method}(:user)"
it "registers an offense for #{namespaced_forbidden_method}" do
diff --git a/spec/rubocop/cop/rspec/factory_bot/inline_association_spec.rb b/spec/rubocop/cop/rspec/factory_bot/inline_association_spec.rb
index 33fdaaee3c7..0e6af71ea3e 100644
--- a/spec/rubocop/cop/rspec/factory_bot/inline_association_spec.rb
+++ b/spec/rubocop/cop/rspec/factory_bot/inline_association_spec.rb
@@ -7,8 +7,6 @@ require 'rubocop'
require_relative '../../../../../rubocop/cop/rspec/factory_bot/inline_association'
RSpec.describe RuboCop::Cop::RSpec::FactoryBot::InlineAssociation do
- include CopHelper
-
subject(:cop) { described_class.new }
shared_examples 'offense' do |code_snippet, autocorrected|
@@ -17,27 +15,31 @@ RSpec.describe RuboCop::Cop::RSpec::FactoryBot::InlineAssociation do
let(:offense_marker) { '^' * code_snippet.size }
let(:offense_msg) { msg(type) }
let(:offense) { "#{offense_marker} #{offense_msg}" }
- let(:pristine_source) { source.sub(offense, '') }
let(:source) do
<<~RUBY
- FactoryBot.define do
- factory :project do
- attribute { #{code_snippet} }
- #{offense}
- end
+ FactoryBot.define do
+ factory :project do
+ attribute { #{code_snippet} }
+ #{offense}
end
+ end
RUBY
end
- it 'registers an offense' do
- expect_offense(source)
+ let(:corrected_source) do
+ <<~RUBY
+ FactoryBot.define do
+ factory :project do
+ attribute { #{autocorrected} }
+ end
+ end
+ RUBY
end
- it 'autocorrects the source' do
- corrected = autocorrect_source(pristine_source)
+ it 'registers an offense and corrects', :aggregate_failures do
+ expect_offense(source)
- expect(corrected).not_to include(code_snippet)
- expect(corrected).to include(autocorrected)
+ expect_correction(corrected_source)
end
end
diff --git a/spec/rubocop/cop/rspec/have_gitlab_http_status_spec.rb b/spec/rubocop/cop/rspec/have_gitlab_http_status_spec.rb
index f6040350dc0..c2d97c8992a 100644
--- a/spec/rubocop/cop/rspec/have_gitlab_http_status_spec.rb
+++ b/spec/rubocop/cop/rspec/have_gitlab_http_status_spec.rb
@@ -4,50 +4,42 @@ require 'fast_spec_helper'
require 'rspec-parameterized'
require 'rubocop'
-require 'rubocop/rspec/support'
-
require_relative '../../../../rubocop/cop/rspec/have_gitlab_http_status'
RSpec.describe RuboCop::Cop::RSpec::HaveGitlabHttpStatus do
- include CopHelper
-
using RSpec::Parameterized::TableSyntax
let(:source_file) { 'spec/foo_spec.rb' }
subject(:cop) { described_class.new }
- shared_examples 'offense' do |code|
- it 'registers an offense' do
- inspect_source(code, source_file)
+ shared_examples 'offense' do |bad, good|
+ it 'registers an offense', :aggregate_failures do
+ expect_offense(<<~CODE, node: bad)
+ %{node}
+ ^{node} [...]
+ CODE
- expect(cop.offenses.size).to eq(1)
- expect(cop.offenses.map(&:line)).to eq([1])
- expect(cop.highlights).to eq([code])
+ expect_correction(<<~CODE)
+ #{good}
+ CODE
end
end
shared_examples 'no offense' do |code|
it 'does not register an offense' do
- inspect_source(code)
-
- expect(cop.offenses).to be_empty
- end
- end
-
- shared_examples 'autocorrect' do |bad, good|
- it 'autocorrects' do
- autocorrected = autocorrect_source(bad, source_file)
-
- expect(autocorrected).to eql(good)
+ expect_no_offenses(code)
end
end
- shared_examples 'no autocorrect' do |code|
+ shared_examples 'offense with no autocorrect' do |code|
it 'does not autocorrect' do
- autocorrected = autocorrect_source(code, source_file)
+ expect_offense(<<~CODE, node: code)
+ %{node}
+ ^{node} [...]
+ CODE
- expect(autocorrected).to eql(code)
+ expect_no_corrections
end
end
@@ -64,10 +56,8 @@ RSpec.describe RuboCop::Cop::RSpec::HaveGitlabHttpStatus do
end
with_them do
- include_examples 'offense', params[:bad]
+ include_examples 'offense', params[:bad], params[:good]
include_examples 'no offense', params[:good]
- include_examples 'autocorrect', params[:bad], params[:good]
- include_examples 'no autocorrect', params[:good]
end
end
@@ -77,10 +67,8 @@ RSpec.describe RuboCop::Cop::RSpec::HaveGitlabHttpStatus do
end
with_them do
- include_examples 'offense', params[:bad]
- include_examples 'offense', params[:good]
- include_examples 'autocorrect', params[:bad], params[:good]
- include_examples 'no autocorrect', params[:good]
+ include_examples 'offense', params[:bad], params[:good]
+ include_examples 'offense with no autocorrect', params[:good]
end
end
@@ -114,7 +102,6 @@ RSpec.describe RuboCop::Cop::RSpec::HaveGitlabHttpStatus do
with_them do
include_examples 'no offense', params[:code]
- include_examples 'no autocorrect', params[:code]
end
end
end
diff --git a/spec/rubocop/cop/rspec/htt_party_basic_auth_spec.rb b/spec/rubocop/cop/rspec/htt_party_basic_auth_spec.rb
index 6e9e436602c..eac6ceb3ddf 100644
--- a/spec/rubocop/cop/rspec/htt_party_basic_auth_spec.rb
+++ b/spec/rubocop/cop/rspec/htt_party_basic_auth_spec.rb
@@ -5,12 +5,10 @@ require 'fast_spec_helper'
require_relative '../../../../rubocop/cop/rspec/httparty_basic_auth'
RSpec.describe RuboCop::Cop::RSpec::HTTPartyBasicAuth do
- include CopHelper
-
subject(:cop) { described_class.new }
context 'when passing `basic_auth: { user: ... }`' do
- it 'registers an offence' do
+ it 'registers an offense and corrects', :aggregate_failures do
expect_offense(<<~SOURCE, 'spec/foo.rb')
HTTParty.put(
url,
@@ -19,17 +17,19 @@ RSpec.describe RuboCop::Cop::RSpec::HTTPartyBasicAuth do
body: body
)
SOURCE
- end
- it 'can autocorrect the source' do
- bad = 'HTTParty.put(url, basic_auth: { user: user, password: token })'
- good = 'HTTParty.put(url, basic_auth: { username: user, password: token })'
- expect(autocorrect_source(bad)).to eq(good)
+ expect_correction(<<~SOURCE)
+ HTTParty.put(
+ url,
+ basic_auth: { username: user, password: token },
+ body: body
+ )
+ SOURCE
end
end
context 'when passing `basic_auth: { username: ... }`' do
- it 'does not register an offence' do
+ it 'does not register an offense' do
expect_no_offenses(<<~SOURCE, 'spec/frontend/fixtures/foo.rb')
HTTParty.put(
url,
diff --git a/spec/rubocop/cop/rspec/modify_sidekiq_middleware_spec.rb b/spec/rubocop/cop/rspec/modify_sidekiq_middleware_spec.rb
index d9e3ca5741c..ffabbae90dc 100644
--- a/spec/rubocop/cop/rspec/modify_sidekiq_middleware_spec.rb
+++ b/spec/rubocop/cop/rspec/modify_sidekiq_middleware_spec.rb
@@ -5,33 +5,20 @@ require 'rubocop'
require_relative '../../../../rubocop/cop/rspec/modify_sidekiq_middleware'
RSpec.describe RuboCop::Cop::RSpec::ModifySidekiqMiddleware do
- include CopHelper
-
subject(:cop) { described_class.new }
- let(:source) do
- <<~SRC
- Sidekiq::Testing.server_middleware do |chain|
- chain.add(MyCustomMiddleware)
- end
- SRC
- end
-
- let(:corrected) do
- <<~SRC
- with_sidekiq_server_middleware do |chain|
- chain.add(MyCustomMiddleware)
- end
- SRC
- end
-
- it 'registers an offence' do
- inspect_source(source)
-
- expect(cop.offenses.size).to eq(1)
- end
-
- it 'can autocorrect the source' do
- expect(autocorrect_source(source)).to eq(corrected)
+ it 'registers an offense and corrects', :aggregate_failures do
+ expect_offense(<<~CODE)
+ Sidekiq::Testing.server_middleware do |chain|
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't modify global sidekiq middleware, [...]
+ chain.add(MyCustomMiddleware)
+ end
+ CODE
+
+ expect_correction(<<~CODE)
+ with_sidekiq_server_middleware do |chain|
+ chain.add(MyCustomMiddleware)
+ end
+ CODE
end
end
diff --git a/spec/rubocop/cop/rspec/timecop_freeze_spec.rb b/spec/rubocop/cop/rspec/timecop_freeze_spec.rb
index b1cf82492e4..939623f8299 100644
--- a/spec/rubocop/cop/rspec/timecop_freeze_spec.rb
+++ b/spec/rubocop/cop/rspec/timecop_freeze_spec.rb
@@ -3,50 +3,29 @@
require 'fast_spec_helper'
require 'rubocop'
-require 'rubocop/rspec/support'
-
require_relative '../../../../rubocop/cop/rspec/timecop_freeze'
RSpec.describe RuboCop::Cop::RSpec::TimecopFreeze do
- include CopHelper
-
subject(:cop) { described_class.new }
context 'when calling Timecop.freeze' do
- let(:source) do
- <<~SRC
- Timecop.freeze(Time.current) { example.run }
- SRC
- end
-
- let(:corrected_source) do
- <<~SRC
- freeze_time(Time.current) { example.run }
- SRC
- end
-
- it 'registers an offence' do
- inspect_source(source)
-
- expect(cop.offenses.size).to eq(1)
- end
-
- it 'can autocorrect the source' do
- expect(autocorrect_source(source)).to eq(corrected_source)
+ it 'registers an offense and corrects', :aggregate_failures do
+ expect_offense(<<~CODE)
+ Timecop.freeze(Time.current) { example.run }
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Do not use `Timecop.freeze`, use `freeze_time` instead. [...]
+ CODE
+
+ expect_correction(<<~CODE)
+ freeze_time(Time.current) { example.run }
+ CODE
end
end
context 'when calling a different method on Timecop' do
- let(:source) do
- <<~SRC
- Timecop.travel(Time.current)
- SRC
- end
-
- it 'does not register an offence' do
- inspect_source(source)
-
- expect(cop.offenses).to be_empty
+ it 'does not register an offense' do
+ expect_no_offenses(<<~CODE)
+ Timecop.travel(Time.current)
+ CODE
end
end
end
diff --git a/spec/rubocop/cop/rspec/timecop_travel_spec.rb b/spec/rubocop/cop/rspec/timecop_travel_spec.rb
index 2ee8bfe9ad7..476e45e69a6 100644
--- a/spec/rubocop/cop/rspec/timecop_travel_spec.rb
+++ b/spec/rubocop/cop/rspec/timecop_travel_spec.rb
@@ -3,50 +3,29 @@
require 'fast_spec_helper'
require 'rubocop'
-require 'rubocop/rspec/support'
-
require_relative '../../../../rubocop/cop/rspec/timecop_travel'
RSpec.describe RuboCop::Cop::RSpec::TimecopTravel do
- include CopHelper
-
subject(:cop) { described_class.new }
context 'when calling Timecop.travel' do
- let(:source) do
- <<~SRC
- Timecop.travel(1.day.ago) { create(:issue) }
- SRC
- end
-
- let(:corrected_source) do
- <<~SRC
- travel_to(1.day.ago) { create(:issue) }
- SRC
- end
-
- it 'registers an offence' do
- inspect_source(source)
-
- expect(cop.offenses.size).to eq(1)
- end
-
- it 'can autocorrect the source' do
- expect(autocorrect_source(source)).to eq(corrected_source)
+ it 'registers an offense and corrects', :aggregate_failures do
+ expect_offense(<<~CODE)
+ Timecop.travel(1.day.ago) { create(:issue) }
+ ^^^^^^^^^^^^^^^^^^^^^^^^^ Do not use `Timecop.travel`, use `travel_to` instead. [...]
+ CODE
+
+ expect_correction(<<~CODE)
+ travel_to(1.day.ago) { create(:issue) }
+ CODE
end
end
context 'when calling a different method on Timecop' do
- let(:source) do
- <<~SRC
- Timecop.freeze { create(:issue) }
- SRC
- end
-
- it 'does not register an offence' do
- inspect_source(source)
-
- expect(cop.offenses).to be_empty
+ it 'does not register an offense' do
+ expect_no_offenses(<<~CODE)
+ Timecop.freeze { create(:issue) }
+ CODE
end
end
end
diff --git a/spec/rubocop/cop/rspec/top_level_describe_path_spec.rb b/spec/rubocop/cop/rspec/top_level_describe_path_spec.rb
index 4936936836d..23531cd0201 100644
--- a/spec/rubocop/cop/rspec/top_level_describe_path_spec.rb
+++ b/spec/rubocop/cop/rspec/top_level_describe_path_spec.rb
@@ -5,8 +5,6 @@ require 'rubocop'
require_relative '../../../../rubocop/cop/rspec/top_level_describe_path'
RSpec.describe RuboCop::Cop::RSpec::TopLevelDescribePath do
- include CopHelper
-
subject(:cop) { described_class.new }
context 'when the file ends in _spec.rb' do
diff --git a/spec/rubocop/cop/ruby_interpolation_in_translation_spec.rb b/spec/rubocop/cop/ruby_interpolation_in_translation_spec.rb
index a6a44b3fa68..cacf0a1b67d 100644
--- a/spec/rubocop/cop/ruby_interpolation_in_translation_spec.rb
+++ b/spec/rubocop/cop/ruby_interpolation_in_translation_spec.rb
@@ -1,68 +1,51 @@
# frozen_string_literal: true
require 'fast_spec_helper'
-
require 'rubocop'
-require 'rubocop/rspec/support'
require_relative '../../../rubocop/cop/ruby_interpolation_in_translation'
# Disabling interpolation check as we deliberately want to have #{} in strings.
# rubocop:disable Lint/InterpolationCheck
RSpec.describe RuboCop::Cop::RubyInterpolationInTranslation do
- subject(:cop) { described_class.new }
+ let(:msg) { "Don't use ruby interpolation \#{} inside translated strings, instead use %{}" }
- it 'does not add an offence for a regular messages' do
- inspect_source('_("Hello world")')
+ subject(:cop) { described_class.new }
- expect(cop.offenses).to be_empty
+ it 'does not add an offense for a regular messages' do
+ expect_no_offenses('_("Hello world")')
end
- it 'adds the correct offence when using interpolation in a string' do
- inspect_source('_("Hello #{world}")')
-
- offense = cop.offenses.first
-
- expect(offense.location.source).to eq('#{world}')
- expect(offense.message).to eq('Don\'t use ruby interpolation #{} inside translated strings, instead use %{}')
+ it 'adds the correct offense when using interpolation in a string' do
+ expect_offense(<<~CODE)
+ _("Hello \#{world}")
+ ^^^^^ #{msg}
+ ^^^^^^^^ #{msg}
+ CODE
end
it 'detects when using a ruby interpolation in the first argument of a pluralized string' do
- inspect_source('n_("Hello #{world}", "Hello world")')
-
- expect(cop.offenses).not_to be_empty
+ expect_offense(<<~CODE)
+ n_("Hello \#{world}", "Hello world")
+ ^^^^^ #{msg}
+ ^^^^^^^^ #{msg}
+ CODE
end
it 'detects when using a ruby interpolation in the second argument of a pluralized string' do
- inspect_source('n_("Hello world", "Hello #{world}")')
-
- expect(cop.offenses).not_to be_empty
+ expect_offense(<<~CODE)
+ n_("Hello world", "Hello \#{world}")
+ ^^^^^ #{msg}
+ ^^^^^^^^ #{msg}
+ CODE
end
it 'detects when using interpolation in a namespaced translation' do
- inspect_source('s_("Hello|#{world}")')
-
- expect(cop.offenses).not_to be_empty
- end
-
- it 'does not add an offence for messages defined over multiple lines' do
- source = <<~SRC
- _("Hello "\
- "world ")
- SRC
-
- inspect_source(source)
- expect(cop.offenses).to be_empty
- end
-
- it 'adds an offence for violations in a message defined over multiple lines' do
- source = <<~SRC
- _("Hello "\
- "\#{world} ")
- SRC
-
- inspect_source(source)
- expect(cop.offenses).not_to be_empty
+ expect_offense(<<~CODE)
+ s_("Hello|\#{world}")
+ ^^^^^ #{msg}
+ ^^^^^^^^ #{msg}
+ CODE
end
end
# rubocop:enable Lint/InterpolationCheck
diff --git a/spec/rubocop/cop/safe_params_spec.rb b/spec/rubocop/cop/safe_params_spec.rb
index c8f6768c4bb..62f8e542d86 100644
--- a/spec/rubocop/cop/safe_params_spec.rb
+++ b/spec/rubocop/cop/safe_params_spec.rb
@@ -2,12 +2,9 @@
require 'fast_spec_helper'
require 'rubocop'
-require 'rubocop/rspec/support'
require_relative '../../../rubocop/cop/safe_params'
RSpec.describe RuboCop::Cop::SafeParams do
- include CopHelper
-
subject(:cop) { described_class.new }
it 'flags the params as an argument of url_for' do
diff --git a/spec/rubocop/cop/scalability/bulk_perform_with_context_spec.rb b/spec/rubocop/cop/scalability/bulk_perform_with_context_spec.rb
index 6e526f7ad8f..a19ddf9dbe6 100644
--- a/spec/rubocop/cop/scalability/bulk_perform_with_context_spec.rb
+++ b/spec/rubocop/cop/scalability/bulk_perform_with_context_spec.rb
@@ -5,54 +5,44 @@ require 'rubocop'
require_relative '../../../../rubocop/cop/scalability/bulk_perform_with_context'
RSpec.describe RuboCop::Cop::Scalability::BulkPerformWithContext do
- include CopHelper
-
subject(:cop) { described_class.new }
it "adds an offense when calling bulk_perform_async" do
- inspect_source(<<~CODE)
+ expect_offense(<<~CODE)
Worker.bulk_perform_async(args)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer using `Worker.bulk_perform_async_with_contexts` [...]
CODE
-
- expect(cop.offenses.size).to eq(1)
end
it "adds an offense when calling bulk_perform_in" do
- inspect_source(<<~CODE)
+ expect_offense(<<~CODE)
diffs.each_batch(of: BATCH_SIZE) do |relation, index|
ids = relation.pluck_primary_key.map { |id| [id] }
DeleteDiffFilesWorker.bulk_perform_in(index * 5.minutes, ids)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer using `Worker.bulk_perform_async_with_contexts` [...]
end
CODE
-
- expect(cop.offenses.size).to eq(1)
end
it "does not add an offense for migrations" do
allow(cop).to receive(:in_migration?).and_return(true)
- inspect_source(<<~CODE)
+ expect_no_offenses(<<~CODE)
Worker.bulk_perform_in(args)
CODE
-
- expect(cop.offenses.size).to eq(0)
end
it "does not add an offence for specs" do
allow(cop).to receive(:in_spec?).and_return(true)
- inspect_source(<<~CODE)
+ expect_no_offenses(<<~CODE)
Worker.bulk_perform_in(args)
CODE
-
- expect(cop.offenses.size).to eq(0)
end
it "does not add an offense for scheduling BackgroundMigrations" do
- inspect_source(<<~CODE)
+ expect_no_offenses(<<~CODE)
BackgroundMigrationWorker.bulk_perform_in(args)
CODE
-
- expect(cop.offenses.size).to eq(0)
end
end
diff --git a/spec/rubocop/cop/scalability/cron_worker_context_spec.rb b/spec/rubocop/cop/scalability/cron_worker_context_spec.rb
index 4699e06e9cf..11b2b82d2f5 100644
--- a/spec/rubocop/cop/scalability/cron_worker_context_spec.rb
+++ b/spec/rubocop/cop/scalability/cron_worker_context_spec.rb
@@ -5,18 +5,15 @@ require 'rubocop'
require_relative '../../../../rubocop/cop/scalability/cron_worker_context'
RSpec.describe RuboCop::Cop::Scalability::CronWorkerContext do
- include CopHelper
-
subject(:cop) { described_class.new }
it 'adds an offense when including CronjobQueue' do
- inspect_source(<<~CODE)
+ expect_offense(<<~CODE)
class SomeWorker
include CronjobQueue
+ ^^^^^^^^^^^^ Manually define an ApplicationContext for cronjob-workers.[...]
end
CODE
-
- expect(cop.offenses.size).to eq(1)
end
it 'does not add offenses for other workers' do
diff --git a/spec/rubocop/cop/scalability/file_uploads_spec.rb b/spec/rubocop/cop/scalability/file_uploads_spec.rb
index 78ff7fea55c..bda5c056b03 100644
--- a/spec/rubocop/cop/scalability/file_uploads_spec.rb
+++ b/spec/rubocop/cop/scalability/file_uploads_spec.rb
@@ -5,8 +5,6 @@ require 'rubocop'
require_relative '../../../../rubocop/cop/scalability/file_uploads'
RSpec.describe RuboCop::Cop::Scalability::FileUploads do
- include CopHelper
-
subject(:cop) { described_class.new }
let(:message) { 'Do not upload files without workhorse acceleration. Please refer to https://docs.gitlab.com/ee/development/uploads.html' }
diff --git a/spec/rubocop/cop/scalability/idempotent_worker_spec.rb b/spec/rubocop/cop/scalability/idempotent_worker_spec.rb
index 666122a9de4..729f2613697 100644
--- a/spec/rubocop/cop/scalability/idempotent_worker_spec.rb
+++ b/spec/rubocop/cop/scalability/idempotent_worker_spec.rb
@@ -5,8 +5,6 @@ require 'rubocop'
require_relative '../../../../rubocop/cop/scalability/idempotent_worker'
RSpec.describe RuboCop::Cop::Scalability::IdempotentWorker do
- include CopHelper
-
subject(:cop) { described_class.new }
before do
@@ -16,21 +14,18 @@ RSpec.describe RuboCop::Cop::Scalability::IdempotentWorker do
end
it 'adds an offense when not defining idempotent method' do
- inspect_source(<<~CODE)
+ expect_offense(<<~CODE)
class SomeWorker
+ ^^^^^^^^^^^^^^^^ Avoid adding not idempotent workers.[...]
end
CODE
-
- expect(cop.offenses.size).to eq(1)
end
it 'adds an offense when not defining idempotent method' do
- inspect_source(<<~CODE)
+ expect_no_offenses(<<~CODE)
class SomeWorker
idempotent!
end
CODE
-
- expect(cop.offenses.size).to be_zero
end
end
diff --git a/spec/rubocop/cop/static_translation_definition_spec.rb b/spec/rubocop/cop/static_translation_definition_spec.rb
index 8a38a318999..8656b07a6e4 100644
--- a/spec/rubocop/cop/static_translation_definition_spec.rb
+++ b/spec/rubocop/cop/static_translation_definition_spec.rb
@@ -8,78 +8,76 @@ require 'rspec-parameterized'
require_relative '../../../rubocop/cop/static_translation_definition'
RSpec.describe RuboCop::Cop::StaticTranslationDefinition do
- include CopHelper
-
using RSpec::Parameterized::TableSyntax
+ let(:msg) do
+ "The text you're translating will be already in the translated form when it's assigned to the constant. " \
+ "When a users changes the locale, these texts won't be translated again. " \
+ "Consider moving the translation logic to a method."
+ end
+
subject(:cop) { described_class.new }
- shared_examples 'offense' do |code, highlight, line|
+ shared_examples 'offense' do |code|
it 'registers an offense' do
- inspect_source(code)
-
- expect(cop.offenses.size).to eq(1)
- expect(cop.offenses.map(&:line)).to eq([line])
- expect(cop.highlights).to eq([highlight])
+ expect_offense(code)
end
end
shared_examples 'no offense' do |code|
it 'does not register an offense' do
- inspect_source(code)
-
- expect(cop.offenses).to be_empty
+ expect_no_offenses(code)
end
end
describe 'offenses' do
- where(:code, :highlight, :line) do
+ where(:code) do
[
- ['A = _("a")', '_("a")', 1],
- ['B = s_("b")', 's_("b")', 1],
- ['C = n_("c")', 'n_("c")', 1],
- [
- <<~CODE,
- class MyClass
- def self.translations
- @cache ||= { hello: _("hello") }
- end
+ <<~CODE,
+ A = _("a")
+ ^^^^^^ #{msg}
+ CODE
+ <<~CODE,
+ B = s_("b")
+ ^^^^^^^ #{msg}
+ CODE
+ <<~CODE,
+ C = n_("c")
+ ^^^^^^^ #{msg}
+ CODE
+ <<~CODE,
+ class MyClass
+ def self.translations
+ @cache ||= { hello: _("hello") }
+ ^^^^^^^^^^ #{msg}
end
- CODE
- '_("hello")',
- 3
- ],
- [
- <<~CODE,
- module MyModule
- A = {
- b: {
- c: _("a")
- }
+ end
+ CODE
+ <<~CODE,
+ module MyModule
+ A = {
+ b: {
+ c: _("a")
+ ^^^^^^ #{msg}
}
- end
- CODE
- '_("a")',
- 4
- ],
- [
- <<~CODE,
- class MyClass
- B = [
- [
- s_("a")
- ]
+ }
+ end
+ CODE
+ <<~CODE
+ class MyClass
+ B = [
+ [
+ s_("a")
+ ^^^^^^^ #{msg}
]
- end
- CODE
- 's_("a")',
- 4
- ]
+ ]
+ end
+ CODE
]
end
with_them do
- include_examples 'offense', params[:code], params[:highlight], params[:line]
+ include_examples 'offense', params[:code]
end
end
diff --git a/spec/rubocop/cop/usage_data/distinct_count_by_large_foreign_key_spec.rb b/spec/rubocop/cop/usage_data/distinct_count_by_large_foreign_key_spec.rb
index 1c90df798a5..b6711effe9e 100644
--- a/spec/rubocop/cop/usage_data/distinct_count_by_large_foreign_key_spec.rb
+++ b/spec/rubocop/cop/usage_data/distinct_count_by_large_foreign_key_spec.rb
@@ -1,17 +1,13 @@
# frozen_string_literal: true
require 'fast_spec_helper'
-
require 'rubocop'
-require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/usage_data/distinct_count_by_large_foreign_key'
RSpec.describe RuboCop::Cop::UsageData::DistinctCountByLargeForeignKey do
- include CopHelper
-
let(:allowed_foreign_keys) { [:author_id, :user_id, :'merge_requests.target_project_id'] }
-
+ let(:msg) { 'Avoid doing `distinct_count` on foreign keys for large tables having above 100 million rows.' }
let(:config) do
RuboCop::Config.new('UsageData/DistinctCountByLargeForeignKey' => {
'AllowedForeignKeys' => allowed_foreign_keys
@@ -21,36 +17,32 @@ RSpec.describe RuboCop::Cop::UsageData::DistinctCountByLargeForeignKey do
subject(:cop) { described_class.new(config) }
context 'when counting by disallowed key' do
- it 'registers an offence' do
- inspect_source('distinct_count(Issue, :creator_id)')
-
- expect(cop.offenses.size).to eq(1)
+ it 'registers an offense' do
+ expect_offense(<<~CODE)
+ distinct_count(Issue, :creator_id)
+ ^^^^^^^^^^^^^^ #{msg}
+ CODE
end
- it 'does not register an offence when batch is false' do
- inspect_source('distinct_count(Issue, :creator_id, batch: false)')
-
- expect(cop.offenses).to be_empty
+ it 'does not register an offense when batch is false' do
+ expect_no_offenses('distinct_count(Issue, :creator_id, batch: false)')
end
- it 'register an offence when batch is true' do
- inspect_source('distinct_count(Issue, :creator_id, batch: true)')
-
- expect(cop.offenses.size).to eq(1)
+ it 'registers an offense when batch is true' do
+ expect_offense(<<~CODE)
+ distinct_count(Issue, :creator_id, batch: true)
+ ^^^^^^^^^^^^^^ #{msg}
+ CODE
end
end
context 'when calling by allowed key' do
- it 'does not register an offence with symbol' do
- inspect_source('distinct_count(Issue, :author_id)')
-
- expect(cop.offenses).to be_empty
+ it 'does not register an offense with symbol' do
+ expect_no_offenses('distinct_count(Issue, :author_id)')
end
- it 'does not register an offence with string' do
- inspect_source("distinct_count(Issue, 'merge_requests.target_project_id')")
-
- expect(cop.offenses).to be_empty
+ it 'does not register an offense with string' do
+ expect_no_offenses("distinct_count(Issue, 'merge_requests.target_project_id')")
end
end
end
diff --git a/spec/rubocop/cop/usage_data/large_table_spec.rb b/spec/rubocop/cop/usage_data/large_table_spec.rb
index 638e8c67dc8..26bd4e61625 100644
--- a/spec/rubocop/cop/usage_data/large_table_spec.rb
+++ b/spec/rubocop/cop/usage_data/large_table_spec.rb
@@ -1,18 +1,15 @@
# frozen_string_literal: true
require 'fast_spec_helper'
-
require 'rubocop'
-require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/usage_data/large_table'
RSpec.describe RuboCop::Cop::UsageData::LargeTable do
- include CopHelper
-
let(:large_tables) { %i[Rails Time] }
let(:count_methods) { %i[count distinct_count] }
let(:allowed_methods) { %i[minimum maximum] }
+ let(:msg) { 'Use one of the count, distinct_count methods for counting on' }
let(:config) do
RuboCop::Config.new('UsageData/LargeTable' => {
@@ -31,59 +28,54 @@ RSpec.describe RuboCop::Cop::UsageData::LargeTable do
context 'with large tables' do
context 'when calling Issue.count' do
- it 'register an offence' do
- inspect_source('Issue.count')
-
- expect(cop.offenses.size).to eq(1)
+ it 'registers an offense' do
+ expect_offense(<<~CODE)
+ Issue.count
+ ^^^^^^^^^^^ #{msg} Issue
+ CODE
end
end
context 'when calling Issue.active.count' do
- it 'register an offence' do
- inspect_source('Issue.active.count')
-
- expect(cop.offenses.size).to eq(1)
+ it 'registers an offense' do
+ expect_offense(<<~CODE)
+ Issue.active.count
+ ^^^^^^^^^^^^ #{msg} Issue
+ CODE
end
end
context 'when calling count(Issue)' do
- it 'does not register an offence' do
- inspect_source('count(Issue)')
-
- expect(cop.offenses).to be_empty
+ it 'does not register an offense' do
+ expect_no_offenses('count(Issue)')
end
end
context 'when calling count(Ci::Build.active)' do
- it 'does not register an offence' do
- inspect_source('count(Ci::Build.active)')
-
- expect(cop.offenses).to be_empty
+ it 'does not register an offense' do
+ expect_no_offenses('count(Ci::Build.active)')
end
end
context 'when calling Ci::Build.active.count' do
- it 'register an offence' do
- inspect_source('Ci::Build.active.count')
-
- expect(cop.offenses.size).to eq(1)
+ it 'registers an offense' do
+ expect_offense(<<~CODE)
+ Ci::Build.active.count
+ ^^^^^^^^^^^^^^^^ #{msg} Ci::Build
+ CODE
end
end
context 'when using allowed methods' do
- it 'does not register an offence' do
- inspect_source('Issue.minimum')
-
- expect(cop.offenses).to be_empty
+ it 'does not register an offense' do
+ expect_no_offenses('Issue.minimum')
end
end
end
context 'with non related class' do
- it 'does not register an offence' do
- inspect_source('Rails.count')
-
- expect(cop.offenses).to be_empty
+ it 'does not register an offense' do
+ expect_no_offenses('Rails.count')
end
end
end
diff --git a/spec/rubocop/qa_helpers_spec.rb b/spec/rubocop/qa_helpers_spec.rb
index 051817903a8..cf6d2f1a845 100644
--- a/spec/rubocop/qa_helpers_spec.rb
+++ b/spec/rubocop/qa_helpers_spec.rb
@@ -6,7 +6,7 @@ require 'parser/current'
require_relative '../../rubocop/qa_helpers'
RSpec.describe RuboCop::QAHelpers do
- def parse_source(source, path = 'foo.rb')
+ def build_and_parse_source(source, path = 'foo.rb')
buffer = Parser::Source::Buffer.new(path)
buffer.source = source
@@ -24,13 +24,13 @@ RSpec.describe RuboCop::QAHelpers do
describe '#in_qa_file?' do
it 'returns true for a node in the qa/ directory' do
- node = parse_source('10', rails_root_join('qa', 'qa', 'page', 'dashboard', 'groups.rb'))
+ node = build_and_parse_source('10', rails_root_join('qa', 'qa', 'page', 'dashboard', 'groups.rb'))
expect(cop.in_qa_file?(node)).to eq(true)
end
it 'returns false for a node outside the qa/ directory' do
- node = parse_source('10', rails_root_join('app', 'foo', 'foo.rb'))
+ node = build_and_parse_source('10', rails_root_join('app', 'foo', 'foo.rb'))
expect(cop.in_qa_file?(node)).to eq(false)
end
diff --git a/spec/serializers/admin/user_entity_spec.rb b/spec/serializers/admin/user_entity_spec.rb
index 7db49af09c3..42efe0eec54 100644
--- a/spec/serializers/admin/user_entity_spec.rb
+++ b/spec/serializers/admin/user_entity_spec.rb
@@ -22,6 +22,7 @@ RSpec.describe Admin::UserEntity do
:username,
:last_activity_on,
:avatar_url,
+ :note,
:badges,
:projects_count,
:actions
diff --git a/spec/serializers/admin/user_serializer_spec.rb b/spec/serializers/admin/user_serializer_spec.rb
index 719a90384c6..53a9457409c 100644
--- a/spec/serializers/admin/user_serializer_spec.rb
+++ b/spec/serializers/admin/user_serializer_spec.rb
@@ -17,6 +17,7 @@ RSpec.describe Admin::UserSerializer do
:username,
:last_activity_on,
:avatar_url,
+ :note,
:badges,
:projects_count,
:actions
diff --git a/spec/serializers/ci/codequality_mr_diff_entity_spec.rb b/spec/serializers/ci/codequality_mr_diff_entity_spec.rb
new file mode 100644
index 00000000000..82708908d95
--- /dev/null
+++ b/spec/serializers/ci/codequality_mr_diff_entity_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::CodequalityMrDiffEntity do
+ let(:entity) { described_class.new(mr_diff_report) }
+ let(:mr_diff_report) { Gitlab::Ci::Reports::CodequalityMrDiff.new(codequality_report) }
+ let(:codequality_report) { Gitlab::Ci::Reports::CodequalityReports.new }
+ let(:degradation_1) { build(:codequality_degradation_1) }
+ let(:degradation_2) { build(:codequality_degradation_2) }
+
+ describe '#as_json' do
+ subject(:report) { entity.as_json }
+
+ context 'when quality report has degradations' do
+ before do
+ codequality_report.add_degradation(degradation_1)
+ codequality_report.add_degradation(degradation_2)
+ end
+
+ it 'contains correct codequality mr diff report', :aggregate_failures do
+ expect(report[:files].keys).to eq(["file_a.rb"])
+ expect(report[:files]["file_a.rb"].first).to include(:line, :description, :severity)
+ end
+ end
+ end
+end
diff --git a/spec/serializers/ci/codequality_mr_diff_report_serializer_spec.rb b/spec/serializers/ci/codequality_mr_diff_report_serializer_spec.rb
new file mode 100644
index 00000000000..906ca36041f
--- /dev/null
+++ b/spec/serializers/ci/codequality_mr_diff_report_serializer_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::CodequalityMrDiffReportSerializer do
+ let(:serializer) { described_class.new.represent(mr_diff_report) }
+ let(:mr_diff_report) { Gitlab::Ci::Reports::CodequalityMrDiff.new(codequality_report) }
+ let(:codequality_report) { Gitlab::Ci::Reports::CodequalityReports.new }
+ let(:degradation_1) { build(:codequality_degradation_1) }
+ let(:degradation_2) { build(:codequality_degradation_2) }
+
+ describe '#to_json' do
+ subject { serializer.as_json }
+
+ context 'when quality report has degradations' do
+ before do
+ codequality_report.add_degradation(degradation_1)
+ codequality_report.add_degradation(degradation_2)
+ end
+
+ it 'matches the schema' do
+ expect(subject).to match_schema('entities/codequality_mr_diff_report')
+ end
+ end
+
+ context 'when quality report has no degradations' do
+ it 'matches the schema' do
+ expect(subject).to match_schema('entities/codequality_mr_diff_report')
+ end
+ end
+ end
+end
diff --git a/spec/serializers/ci/dag_pipeline_entity_spec.rb b/spec/serializers/ci/dag_pipeline_entity_spec.rb
index e1703b09f97..fdc2f5e1a04 100644
--- a/spec/serializers/ci/dag_pipeline_entity_spec.rb
+++ b/spec/serializers/ci/dag_pipeline_entity_spec.rb
@@ -73,11 +73,11 @@ RSpec.describe Ci::DagPipelineEntity do
end
end
- it 'performs the smallest number of queries' do
+ it 'performs the smallest number of queries', :request_store do
log = ActiveRecord::QueryRecorder.new { subject }
- # stages, project, builds, build_needs
- expect(log.count).to eq 4
+ # stages, project, builds, build_needs, feature_flag
+ expect(log.count).to eq 5
end
it 'contains all the data' do
diff --git a/spec/serializers/ci/pipeline_entity_spec.rb b/spec/serializers/ci/pipeline_entity_spec.rb
new file mode 100644
index 00000000000..6ce3cef5f44
--- /dev/null
+++ b/spec/serializers/ci/pipeline_entity_spec.rb
@@ -0,0 +1,261 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::PipelineEntity do
+ include Gitlab::Routing
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+ let(:request) { double('request', current_user: user) }
+ let(:entity) { described_class.represent(pipeline, request: request) }
+
+ describe '#as_json' do
+ subject { entity.as_json }
+
+ context 'when pipeline is empty' do
+ let(:pipeline) { create(:ci_empty_pipeline) }
+
+ it 'contains required fields' do
+ expect(subject).to include :id, :user, :path, :coverage, :source
+ expect(subject).to include :ref, :commit
+ expect(subject).to include :updated_at, :created_at
+ end
+
+ it 'excludes coverage data when disabled' do
+ entity = described_class
+ .represent(pipeline, request: request, disable_coverage: true)
+
+ expect(entity.as_json).not_to include(:coverage)
+ end
+
+ it 'contains details' do
+ expect(subject).to include :details
+ expect(subject[:details])
+ .to include :duration, :finished_at, :name
+ expect(subject[:details][:status]).to include :icon, :favicon, :text, :label, :tooltip
+ end
+
+ it 'contains flags' do
+ expect(subject).to include :flags
+ expect(subject[:flags])
+ .to include :stuck, :auto_devops, :yaml_errors,
+ :retryable, :cancelable, :merge_request
+ end
+ end
+
+ context 'when default branch not protected' do
+ before do
+ stub_not_protect_default_branch
+ end
+
+ context 'when pipeline is retryable' do
+ let_it_be(:pipeline) do
+ create(:ci_pipeline, status: :success, project: project)
+ end
+
+ before do
+ create(:ci_build, :failed, pipeline: pipeline)
+ end
+
+ it 'does not serialize stage builds' do
+ subject.with_indifferent_access.dig(:details, :stages, 0).tap do |stage|
+ expect(stage).not_to include(:groups, :latest_statuses, :retries)
+ end
+ end
+
+ context 'user has ability to retry pipeline' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'contains retry path' do
+ expect(subject[:retry_path]).to be_present
+ end
+ end
+
+ context 'user does not have ability to retry pipeline' do
+ it 'does not contain retry path' do
+ expect(subject).not_to have_key(:retry_path)
+ end
+ end
+ end
+
+ context 'when pipeline is cancelable' do
+ let_it_be(:pipeline) do
+ create(:ci_pipeline, status: :running, project: project)
+ end
+
+ before do
+ create(:ci_build, :pending, pipeline: pipeline)
+ end
+
+ it 'does not serialize stage builds' do
+ subject.with_indifferent_access.dig(:details, :stages, 0).tap do |stage|
+ expect(stage).not_to include(:groups, :latest_statuses, :retries)
+ end
+ end
+
+ context 'user has ability to cancel pipeline' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'contains cancel path' do
+ expect(subject[:cancel_path]).to be_present
+ end
+ end
+
+ context 'user does not have ability to cancel pipeline' do
+ it 'does not contain cancel path' do
+ expect(subject).not_to have_key(:cancel_path)
+ end
+ end
+ end
+ end
+
+ context 'delete path' do
+ context 'user has ability to delete pipeline' do
+ let(:project) { create(:project, namespace: user.namespace) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ it 'contains delete path' do
+ expect(subject[:delete_path]).to be_present
+ end
+ end
+
+ context 'user does not have ability to delete pipeline' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ it 'does not contain delete path' do
+ expect(subject).not_to have_key(:delete_path)
+ end
+ end
+ end
+
+ context 'when pipeline ref is empty' do
+ let(:pipeline) { create(:ci_empty_pipeline) }
+
+ before do
+ allow(pipeline).to receive(:ref).and_return(nil)
+ end
+
+ it 'does not generate branch path' do
+ expect(subject[:ref][:path]).to be_nil
+ end
+ end
+
+ context 'when pipeline has a failure reason set' do
+ let(:pipeline) { create(:ci_empty_pipeline) }
+
+ before do
+ pipeline.drop!(:config_error)
+ end
+
+ it 'has a correct failure reason' do
+ expect(subject[:failure_reason])
+ .to eq 'CI/CD YAML configuration error!'
+ end
+ end
+
+ context 'when request has a project' do
+ before do
+ allow(request).to receive(:project).and_return(project)
+ end
+
+ context 'when pipeline is detached merge request pipeline' do
+ let_it_be(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
+ let(:project) { merge_request.target_project }
+ let(:pipeline) { merge_request.pipelines_for_merge_request.first }
+
+ it 'makes detached flag true' do
+ expect(subject[:flags][:detached_merge_request_pipeline]).to be_truthy
+ end
+
+ it 'does not expose source sha and target sha' do
+ expect(subject[:source_sha]).to be_nil
+ expect(subject[:target_sha]).to be_nil
+ end
+
+ context 'when user is a developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'has merge request information' do
+ expect(subject[:merge_request][:iid]).to eq(merge_request.iid)
+
+ expect(project_merge_request_path(project, merge_request))
+ .to include(subject[:merge_request][:path])
+
+ expect(subject[:merge_request][:title]).to eq(merge_request.title)
+
+ expect(subject[:merge_request][:source_branch])
+ .to eq(merge_request.source_branch)
+
+ expect(project_commits_path(project, merge_request.source_branch))
+ .to include(subject[:merge_request][:source_branch_path])
+
+ expect(subject[:merge_request][:target_branch])
+ .to eq(merge_request.target_branch)
+
+ expect(project_commits_path(project, merge_request.target_branch))
+ .to include(subject[:merge_request][:target_branch_path])
+ end
+ end
+
+ context 'when user is an external user' do
+ it 'has no merge request information' do
+ expect(subject[:merge_request]).to be_nil
+ end
+ end
+ end
+
+ context 'when pipeline is merge request pipeline' do
+ let_it_be(:merge_request) { create(:merge_request, :with_merge_request_pipeline, merge_sha: 'abc') }
+ let(:project) { merge_request.target_project }
+ let(:pipeline) { merge_request.pipelines_for_merge_request.first }
+
+ it 'makes detached flag false' do
+ expect(subject[:flags][:detached_merge_request_pipeline]).to be_falsy
+ end
+
+ it 'makes atached flag true' do
+ expect(subject[:flags][:merge_request_pipeline]).to be_truthy
+ end
+
+ it 'exposes source sha and target sha' do
+ expect(subject[:source_sha]).to be_present
+ expect(subject[:target_sha]).to be_present
+ end
+
+ it 'exposes merge request event type' do
+ expect(subject[:merge_request_event_type]).to be_present
+ end
+ end
+ end
+
+ context 'when pipeline has failed builds' do
+ let_it_be(:pipeline) { create(:ci_pipeline, user: user) }
+ let_it_be(:build) { create(:ci_build, :success, pipeline: pipeline) }
+ let_it_be(:failed_1) { create(:ci_build, :failed, pipeline: pipeline) }
+ let_it_be(:failed_2) { create(:ci_build, :failed, pipeline: pipeline) }
+
+ context 'when the user can retry the pipeline' do
+ it 'exposes these failed builds' do
+ allow(entity).to receive(:can_retry?).and_return(true)
+
+ expect(subject[:failed_builds].map { |b| b[:id] }).to contain_exactly(failed_1.id, failed_2.id)
+ end
+ end
+
+ context 'when the user cannot retry the pipeline' do
+ it 'is nil' do
+ allow(entity).to receive(:can_retry?).and_return(false)
+
+ expect(subject[:failed_builds]).to be_nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/serializers/codequality_degradation_entity_spec.rb b/spec/serializers/codequality_degradation_entity_spec.rb
index fc969195e35..315f00baa72 100644
--- a/spec/serializers/codequality_degradation_entity_spec.rb
+++ b/spec/serializers/codequality_degradation_entity_spec.rb
@@ -10,77 +10,24 @@ RSpec.describe CodequalityDegradationEntity do
context 'when codequality contains an error' do
context 'when line is included in location' do
- let(:codequality_degradation) 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"
- }.with_indifferent_access
- end
+ let(:codequality_degradation) { build(:codequality_degradation_2) }
it 'contains correct codequality degradation details', :aggregate_failures do
expect(subject[:description]).to eq("Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.")
expect(subject[:severity]).to eq("major")
- expect(subject[:file_path]).to eq("foo.rb")
+ expect(subject[:file_path]).to eq("file_a.rb")
expect(subject[:line]).to eq(10)
end
end
context 'when line is included in positions' do
- let(:codequality_degradation) do
- {
- "type": "Issue",
- "check_name": "Rubocop/Metrics/ParameterLists",
- "description": "Avoid parameter lists longer than 5 parameters. [12/5]",
- "categories": [
- "Complexity"
- ],
- "remediation_points": 550000,
- "location": {
- "path": "foo.rb",
- "positions": {
- "begin": {
- "column": 24,
- "line": 14
- },
- "end": {
- "column": 49,
- "line": 14
- }
- }
- },
- "content": {
- "body": "This cop checks for methods with too many parameters.\nThe maximum number of parameters is configurable.\nKeyword arguments can optionally be excluded from the total count."
- },
- "engine_name": "rubocop",
- "fingerprint": "ab5f8b935886b942d621399f5a2ca16e",
- "severity": "minor"
- }.with_indifferent_access
- end
+ let(:codequality_degradation) { build(:codequality_degradation_3) }
it 'contains correct codequality degradation details', :aggregate_failures do
expect(subject[:description]).to eq("Avoid parameter lists longer than 5 parameters. [12/5]")
expect(subject[:severity]).to eq("minor")
- expect(subject[:file_path]).to eq("foo.rb")
- expect(subject[:line]).to eq(14)
+ expect(subject[:file_path]).to eq("file_b.rb")
+ expect(subject[:line]).to eq(10)
end
end
end
diff --git a/spec/serializers/codequality_reports_comparer_entity_spec.rb b/spec/serializers/codequality_reports_comparer_entity_spec.rb
index 7a79c30cc3f..3c74c20ca19 100644
--- a/spec/serializers/codequality_reports_comparer_entity_spec.rb
+++ b/spec/serializers/codequality_reports_comparer_entity_spec.rb
@@ -7,62 +7,8 @@ RSpec.describe CodequalityReportsComparerEntity do
let(:comparer) { Gitlab::Ci::Reports::CodequalityReportsComparer.new(base_report, head_report) }
let(:base_report) { Gitlab::Ci::Reports::CodequalityReports.new }
let(:head_report) { Gitlab::Ci::Reports::CodequalityReports.new }
- let(:degradation_1) 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"
- }.with_indifferent_access
- end
-
- let(:degradation_2) do
- {
- "type": "Issue",
- "check_name": "Rubocop/Metrics/ParameterLists",
- "description": "Avoid parameter lists longer than 5 parameters. [12/5]",
- "categories": [
- "Complexity"
- ],
- "remediation_points": 550000,
- "location": {
- "path": "foo.rb",
- "positions": {
- "begin": {
- "column": 14,
- "line": 10
- },
- "end": {
- "column": 39,
- "line": 10
- }
- }
- },
- "content": {
- "body": "This cop checks for methods with too many parameters.\nThe maximum number of parameters is configurable.\nKeyword arguments can optionally be excluded from the total count."
- },
- "engine_name": "rubocop",
- "fingerprint": "ab5f8b935886b942d621399f5a2ca16e",
- "severity": "minor"
- }.with_indifferent_access
- end
+ let(:degradation_1) { build(:codequality_degradation_1) }
+ let(:degradation_2) { build(:codequality_degradation_2) }
describe '#as_json' do
subject { entity.as_json }
diff --git a/spec/serializers/codequality_reports_comparer_serializer_spec.rb b/spec/serializers/codequality_reports_comparer_serializer_spec.rb
index 3c47bfb6adc..50c8a69737c 100644
--- a/spec/serializers/codequality_reports_comparer_serializer_spec.rb
+++ b/spec/serializers/codequality_reports_comparer_serializer_spec.rb
@@ -8,62 +8,8 @@ RSpec.describe CodequalityReportsComparerSerializer do
let(:comparer) { Gitlab::Ci::Reports::CodequalityReportsComparer.new(base_report, head_report) }
let(:base_report) { Gitlab::Ci::Reports::CodequalityReports.new }
let(:head_report) { Gitlab::Ci::Reports::CodequalityReports.new }
- let(:degradation_1) 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"
- }.with_indifferent_access
- end
-
- let(:degradation_2) do
- {
- "type": "Issue",
- "check_name": "Rubocop/Metrics/ParameterLists",
- "description": "Avoid parameter lists longer than 5 parameters. [12/5]",
- "categories": [
- "Complexity"
- ],
- "remediation_points": 550000,
- "location": {
- "path": "foo.rb",
- "positions": {
- "begin": {
- "column": 14,
- "line": 10
- },
- "end": {
- "column": 39,
- "line": 10
- }
- }
- },
- "content": {
- "body": "This cop checks for methods with too many parameters.\nThe maximum number of parameters is configurable.\nKeyword arguments can optionally be excluded from the total count."
- },
- "engine_name": "rubocop",
- "fingerprint": "ab5f8b935886b942d621399f5a2ca16e",
- "severity": "minor"
- }.with_indifferent_access
- end
+ let(:degradation_1) { build(:codequality_degradation_1) }
+ let(:degradation_2) { build(:codequality_degradation_2) }
describe '#to_json' do
subject { serializer.as_json }
diff --git a/spec/serializers/diff_file_metadata_entity_spec.rb b/spec/serializers/diff_file_metadata_entity_spec.rb
new file mode 100644
index 00000000000..3ce1ea49677
--- /dev/null
+++ b/spec/serializers/diff_file_metadata_entity_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe DiffFileMetadataEntity do
+ let(:merge_request) { create(:merge_request_with_diffs) }
+ let(:raw_diff_file) { merge_request.merge_request_diff.diffs.raw_diff_files.first }
+ let(:entity) { described_class.new(raw_diff_file) }
+
+ context 'as json' do
+ subject { entity.as_json }
+
+ it 'exposes the expected fields' do
+ expect(subject.keys).to contain_exactly(
+ :added_lines,
+ :removed_lines,
+ :new_path,
+ :old_path,
+ :new_file,
+ :deleted_file,
+ :submodule,
+ :file_identifier_hash,
+ :file_hash
+ )
+ end
+ end
+end
diff --git a/spec/serializers/diffs_entity_spec.rb b/spec/serializers/diffs_entity_spec.rb
index 7569493573b..a7446f14745 100644
--- a/spec/serializers/diffs_entity_spec.rb
+++ b/spec/serializers/diffs_entity_spec.rb
@@ -3,10 +3,11 @@
require 'spec_helper'
RSpec.describe DiffsEntity do
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
+
let(:request) { EntityRequest.new(project: project, current_user: user) }
- let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
let(:merge_request_diffs) { merge_request.merge_request_diffs }
let(:options) do
{ request: request, merge_request: merge_request, merge_request_diffs: merge_request_diffs }
@@ -30,6 +31,14 @@ RSpec.describe DiffsEntity do
)
end
+ context 'broken merge request' do
+ let(:merge_request) { create(:merge_request, :invalid, target_project: project, source_project: project) }
+
+ it 'renders without errors' do
+ expect { subject }.not_to raise_error
+ end
+ end
+
context "when a commit_id is passed" do
let(:commits) { merge_request.commits }
let(:entity) do
diff --git a/spec/serializers/group_group_link_entity_spec.rb b/spec/serializers/group_link/group_group_link_entity_spec.rb
index 9affe4af381..15bcbbcb1d6 100644
--- a/spec/serializers/group_group_link_entity_spec.rb
+++ b/spec/serializers/group_link/group_group_link_entity_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe GroupGroupLinkEntity do
+RSpec.describe GroupLink::GroupGroupLinkEntity do
include_context 'group_group_link'
let_it_be(:current_user) { create(:user) }
@@ -13,15 +13,15 @@ RSpec.describe GroupGroupLinkEntity do
end
it 'matches json schema' do
- expect(entity.to_json).to match_schema('entities/group_group_link')
+ expect(entity.to_json).to match_schema('group_link/group_group_link')
end
- context 'a user with :admin_group_member permissions' do
+ context 'when current user has `:admin_group_member` permissions' do
before do
allow(entity).to receive(:can?).with(current_user, :admin_group_member, shared_group).and_return(true)
end
- it 'sets `can_update` and `can_remove` to `true`' do
+ it 'exposes `can_update` and `can_remove` as `true`' do
json = entity.as_json
expect(json[:can_update]).to be true
diff --git a/spec/serializers/group_group_link_serializer_spec.rb b/spec/serializers/group_link/group_group_link_serializer_spec.rb
index 0d977ea0a9a..a4ca32dae7b 100644
--- a/spec/serializers/group_group_link_serializer_spec.rb
+++ b/spec/serializers/group_link/group_group_link_serializer_spec.rb
@@ -2,12 +2,12 @@
require 'spec_helper'
-RSpec.describe GroupGroupLinkSerializer do
+RSpec.describe GroupLink::GroupGroupLinkSerializer do
include_context 'group_group_link'
subject(:json) { described_class.new.represent(shared_group.shared_with_group_links).to_json }
it 'matches json schema' do
- expect(json).to match_schema('group_group_links')
+ expect(json).to match_schema('group_link/group_group_links')
end
end
diff --git a/spec/serializers/group_link/group_link_entity_spec.rb b/spec/serializers/group_link/group_link_entity_spec.rb
new file mode 100644
index 00000000000..941445feaa2
--- /dev/null
+++ b/spec/serializers/group_link/group_link_entity_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GroupLink::GroupLinkEntity do
+ include_context 'group_group_link'
+
+ let(:entity) { described_class.new(group_group_link) }
+ let(:entity_hash) { entity.as_json }
+
+ it 'matches json schema' do
+ expect(entity.to_json).to match_schema('group_link/group_link')
+ end
+
+ it 'correctly exposes `valid_roles`' do
+ expect(entity_hash[:valid_roles]).to include(Gitlab::Access.options_with_owner)
+ end
+
+ it 'correctly exposes `shared_with_group.avatar_url`' do
+ avatar_url = 'https://gitlab.com/uploads/-/system/group/avatar/24/foobar.png?width=40'
+ allow(shared_with_group).to receive(:avatar_url).with(only_path: false, size: Member::AVATAR_SIZE).and_return(avatar_url)
+
+ expect(entity_hash[:shared_with_group][:avatar_url]).to match(avatar_url)
+ end
+end
diff --git a/spec/serializers/group_link/project_group_link_entity_spec.rb b/spec/serializers/group_link/project_group_link_entity_spec.rb
new file mode 100644
index 00000000000..0bb3d06933b
--- /dev/null
+++ b/spec/serializers/group_link/project_group_link_entity_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GroupLink::ProjectGroupLinkEntity do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project_group_link) { create(:project_group_link) }
+ let(:entity) { described_class.new(project_group_link) }
+
+ before do
+ allow(entity).to receive(:current_user).and_return(current_user)
+ end
+
+ it 'matches json schema' do
+ expect(entity.to_json).to match_schema('group_link/project_group_link')
+ end
+
+ context 'when current user has `admin_project_member` permissions' do
+ before do
+ allow(entity).to receive(:can?).with(current_user, :admin_project_member, project_group_link.project).and_return(true)
+ end
+
+ it 'exposes `can_update` and `can_remove` as `true`' do
+ json = entity.as_json
+
+ expect(json[:can_update]).to be true
+ expect(json[:can_remove]).to be true
+ end
+ end
+end
diff --git a/spec/serializers/group_link/project_group_link_serializer_spec.rb b/spec/serializers/group_link/project_group_link_serializer_spec.rb
new file mode 100644
index 00000000000..ecd5d4d70e4
--- /dev/null
+++ b/spec/serializers/group_link/project_group_link_serializer_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GroupLink::ProjectGroupLinkSerializer do
+ let_it_be(:project_group_links) { create_list(:project_group_link, 1) }
+
+ subject(:json) { described_class.new.represent(project_group_links).to_json }
+
+ it 'matches json schema' do
+ expect(json).to match_schema('group_link/project_group_links')
+ end
+end
diff --git a/spec/serializers/member_entity_spec.rb b/spec/serializers/member_entity_spec.rb
index f34434188c1..883cb511abc 100644
--- a/spec/serializers/member_entity_spec.rb
+++ b/spec/serializers/member_entity_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe MemberEntity do
let_it_be(:current_user) { create(:user) }
- let(:entity) { described_class.new(member, { current_user: current_user, group: group }) }
+ let(:entity) { described_class.new(member, { current_user: current_user, group: group, source: source }) }
let(:entity_hash) { entity.as_json }
shared_examples 'member.json' do
@@ -40,8 +40,27 @@ RSpec.describe MemberEntity do
end
end
+ shared_examples 'is_direct_member' do
+ context 'when `source` is the same as `member.source`' do
+ let(:source) { direct_member_source }
+
+ it 'exposes `is_direct_member` as `true`' do
+ expect(entity_hash[:is_direct_member]).to be(true)
+ end
+ end
+
+ context 'when `source` is not the same as `member.source`' do
+ let(:source) { inherited_member_source }
+
+ it 'exposes `is_direct_member` as `false`' do
+ expect(entity_hash[:is_direct_member]).to be(false)
+ end
+ end
+ end
+
context 'group member' do
let(:group) { create(:group) }
+ let(:source) { group }
let(:member) { GroupMemberPresenter.new(create(:group_member, group: group), current_user: current_user) }
it_behaves_like 'member.json'
@@ -52,11 +71,19 @@ RSpec.describe MemberEntity do
it_behaves_like 'member.json'
it_behaves_like 'invite'
end
+
+ context 'is_direct_member' do
+ let(:direct_member_source) { group }
+ let(:inherited_member_source) { create(:group) }
+
+ it_behaves_like 'is_direct_member'
+ end
end
context 'project member' do
let(:project) { create(:project) }
let(:group) { project.group }
+ let(:source) { project }
let(:member) { ProjectMemberPresenter.new(create(:project_member, project: project), current_user: current_user) }
it_behaves_like 'member.json'
@@ -67,5 +94,12 @@ RSpec.describe MemberEntity do
it_behaves_like 'member.json'
it_behaves_like 'invite'
end
+
+ context 'is_direct_member' do
+ let(:direct_member_source) { project }
+ let(:inherited_member_source) { group }
+
+ it_behaves_like 'is_direct_member'
+ end
end
end
diff --git a/spec/serializers/member_serializer_spec.rb b/spec/serializers/member_serializer_spec.rb
index d3ec45fe9c4..af209c0191f 100644
--- a/spec/serializers/member_serializer_spec.rb
+++ b/spec/serializers/member_serializer_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe MemberSerializer do
let_it_be(:current_user) { create(:user) }
- subject { described_class.new.represent(members, { current_user: current_user, group: group }) }
+ subject { described_class.new.represent(members, { current_user: current_user, group: group, source: source }) }
shared_examples 'members.json' do
it 'matches json schema' do
@@ -17,6 +17,7 @@ RSpec.describe MemberSerializer do
context 'group member' do
let(:group) { create(:group) }
+ let(:source) { group }
let(:members) { present_members(create_list(:group_member, 1, group: group)) }
it_behaves_like 'members.json'
@@ -24,6 +25,7 @@ RSpec.describe MemberSerializer do
context 'project member' do
let(:project) { create(:project) }
+ let(:source) { project }
let(:group) { project.group }
let(:members) { present_members(create_list(:project_member, 1, project: project)) }
diff --git a/spec/serializers/merge_request_basic_entity_spec.rb b/spec/serializers/merge_request_basic_entity_spec.rb
index 4a8bcd72d9c..7aa1f47fda5 100644
--- a/spec/serializers/merge_request_basic_entity_spec.rb
+++ b/spec/serializers/merge_request_basic_entity_spec.rb
@@ -20,23 +20,11 @@ RSpec.describe MergeRequestBasicEntity do
let(:params) { { reviewers: [reviewer] } }
let(:reviewer) { build(:user) }
- context 'when merge_request_reviewers feature is disabled' do
- it 'does not contain assignees attributes' do
- stub_feature_flags(merge_request_reviewers: false)
-
- expect(subject[:reviewers]).to be_nil
- end
- end
-
- context 'when merge_request_reviewers feature is enabled' do
- it 'contains reviewers attributes' do
- stub_feature_flags(merge_request_reviewers: true)
-
- expect(subject[:reviewers].count).to be 1
- expect(subject[:reviewers].first.keys).to include(
- :id, :name, :username, :state, :avatar_url, :web_url
- )
- end
+ it 'contains reviewers attributes' do
+ expect(subject[:reviewers].count).to be 1
+ expect(subject[:reviewers].first.keys).to include(
+ :id, :name, :username, :state, :avatar_url, :web_url
+ )
end
end
end
diff --git a/spec/serializers/merge_request_sidebar_extras_entity_spec.rb b/spec/serializers/merge_request_sidebar_extras_entity_spec.rb
index 74e9956c8a0..58f860097c2 100644
--- a/spec/serializers/merge_request_sidebar_extras_entity_spec.rb
+++ b/spec/serializers/merge_request_sidebar_extras_entity_spec.rb
@@ -35,23 +35,11 @@ RSpec.describe MergeRequestSidebarExtrasEntity do
end
describe '#reviewers' do
- context 'when merge_request_reviewers feature is disabled' do
- it 'does not contain reviewers attributes' do
- stub_feature_flags(merge_request_reviewers: false)
-
- expect(entity[:reviewers]).to be_nil
- end
- end
-
- context 'when merge_request_reviewers feature is enabled' do
- it 'contains reviewers attributes' do
- stub_feature_flags(merge_request_reviewers: true)
-
- expect(entity[:reviewers].count).to be 1
- expect(entity[:reviewers].first.keys).to include(
- :id, :name, :username, :state, :avatar_url, :web_url, :can_merge
- )
- end
+ it 'contains reviewers attributes' do
+ expect(entity[:reviewers].count).to be 1
+ expect(entity[:reviewers].first.keys).to include(
+ :id, :name, :username, :state, :avatar_url, :web_url, :can_merge
+ )
end
end
end
diff --git a/spec/serializers/merge_request_user_entity_spec.rb b/spec/serializers/merge_request_user_entity_spec.rb
index a2ad8e72845..dcd4ef6acfb 100644
--- a/spec/serializers/merge_request_user_entity_spec.rb
+++ b/spec/serializers/merge_request_user_entity_spec.rb
@@ -17,5 +17,23 @@ RSpec.describe MergeRequestUserEntity do
it 'exposes needed attributes' do
expect(subject).to include(:id, :name, :username, :state, :avatar_url, :web_url, :can_merge)
end
+
+ context 'when `status` is not preloaded' do
+ it 'does not expose the availability attribute' do
+ expect(subject).not_to include(:availability)
+ end
+ end
+
+ context 'when `status` is preloaded' do
+ before do
+ user.create_status!(availability: :busy)
+
+ user.status # make sure `status` is loaded
+ end
+
+ it 'exposes the availibility attribute' do
+ expect(subject[:availability]).to eq('busy')
+ end
+ end
end
end
diff --git a/spec/serializers/merge_request_widget_entity_spec.rb b/spec/serializers/merge_request_widget_entity_spec.rb
index 42d843af596..926b33e8e1f 100644
--- a/spec/serializers/merge_request_widget_entity_spec.rb
+++ b/spec/serializers/merge_request_widget_entity_spec.rb
@@ -77,7 +77,7 @@ RSpec.describe MergeRequestWidgetEntity do
end
describe 'codequality report artifacts', :request_store do
- let(:merge_base_pipeline) { create(:ci_pipeline, :with_codequality_report, project: project) }
+ let(:merge_base_pipeline) { create(:ci_pipeline, :with_codequality_reports, project: project) }
before do
project.add_developer(user)
@@ -90,7 +90,7 @@ RSpec.describe MergeRequestWidgetEntity do
end
context 'with report artifacts' do
- let(:pipeline) { create(:ci_pipeline, :with_codequality_report, project: project) }
+ let(:pipeline) { create(:ci_pipeline, :with_codequality_reports, project: project) }
let(:generic_job_id) { pipeline.builds.first.id }
let(:merge_base_job_id) { merge_base_pipeline.builds.first.id }
@@ -100,7 +100,7 @@ RSpec.describe MergeRequestWidgetEntity do
end
context 'on pipelines for merged results' do
- let(:pipeline) { create(:ci_pipeline, :merged_result_pipeline, :with_codequality_report, project: project) }
+ let(:pipeline) { create(:ci_pipeline, :merged_result_pipeline, :with_codequality_reports, project: project) }
it 'returns URLs from the head_pipeline and merge_base_pipeline' do
expect(subject[:codeclimate][:head_path]).to include("/jobs/#{generic_job_id}/artifacts/download?file_type=codequality")
diff --git a/spec/serializers/paginated_diff_entity_spec.rb b/spec/serializers/paginated_diff_entity_spec.rb
index 7090ce1f08d..a8ac89a8481 100644
--- a/spec/serializers/paginated_diff_entity_spec.rb
+++ b/spec/serializers/paginated_diff_entity_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe PaginatedDiffEntity do
let(:user) { create(:user) }
let(:request) { double('request', current_user: user) }
- let(:merge_request) { create(:merge_request, :with_diffs) }
+ let(:merge_request) { create(:merge_request) }
let(:diff_batch) { merge_request.merge_request_diff.diffs_in_batch(2, 3, diff_options: nil) }
let(:options) do
{
diff --git a/spec/serializers/pipeline_details_entity_spec.rb b/spec/serializers/pipeline_details_entity_spec.rb
index 74e91cc9cdd..2f54f45866b 100644
--- a/spec/serializers/pipeline_details_entity_spec.rb
+++ b/spec/serializers/pipeline_details_entity_spec.rb
@@ -10,8 +10,8 @@ RSpec.describe PipelineDetailsEntity do
described_class.represent(pipeline, request: request)
end
- it 'inherrits from PipelineEntity' do
- expect(described_class).to be < PipelineEntity
+ it 'inherits from PipelineEntity' do
+ expect(described_class).to be < Ci::PipelineEntity
end
before do
diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb
deleted file mode 100644
index d7cd13edec8..00000000000
--- a/spec/serializers/pipeline_entity_spec.rb
+++ /dev/null
@@ -1,264 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe PipelineEntity do
- include Gitlab::Routing
-
- let_it_be(:project) { create(:project) }
- let_it_be(:user) { create(:user) }
- let(:request) { double('request') }
-
- before do
- stub_not_protect_default_branch
-
- allow(request).to receive(:current_user).and_return(user)
- allow(request).to receive(:project).and_return(project)
- end
-
- let(:entity) do
- described_class.represent(pipeline, request: request)
- end
-
- describe '#as_json' do
- subject { entity.as_json }
-
- context 'when pipeline is empty' do
- let(:pipeline) { create(:ci_empty_pipeline) }
-
- it 'contains required fields' do
- expect(subject).to include :id, :user, :path, :coverage, :source
- expect(subject).to include :ref, :commit
- expect(subject).to include :updated_at, :created_at
- end
-
- it 'excludes coverage data when disabled' do
- entity = described_class
- .represent(pipeline, request: request, disable_coverage: true)
-
- expect(entity.as_json).not_to include(:coverage)
- end
-
- it 'contains details' do
- expect(subject).to include :details
- expect(subject[:details])
- .to include :duration, :finished_at, :name
- expect(subject[:details][:status]).to include :icon, :favicon, :text, :label, :tooltip
- end
-
- it 'contains flags' do
- expect(subject).to include :flags
- expect(subject[:flags])
- .to include :stuck, :auto_devops, :yaml_errors,
- :retryable, :cancelable, :merge_request
- end
- end
-
- context 'when pipeline is retryable' do
- let(:project) { create(:project) }
-
- let(:pipeline) do
- create(:ci_pipeline, status: :success, project: project)
- end
-
- before do
- create(:ci_build, :failed, pipeline: pipeline)
- end
-
- it 'does not serialize stage builds' do
- subject.with_indifferent_access.dig(:details, :stages, 0).tap do |stage|
- expect(stage).not_to include(:groups, :latest_statuses, :retries)
- end
- end
-
- context 'user has ability to retry pipeline' do
- before do
- project.add_developer(user)
- end
-
- it 'contains retry path' do
- expect(subject[:retry_path]).to be_present
- end
- end
-
- context 'user does not have ability to retry pipeline' do
- it 'does not contain retry path' do
- expect(subject).not_to have_key(:retry_path)
- end
- end
- end
-
- context 'when pipeline is cancelable' do
- let(:project) { create(:project) }
-
- let(:pipeline) do
- create(:ci_pipeline, status: :running, project: project)
- end
-
- before do
- create(:ci_build, :pending, pipeline: pipeline)
- end
-
- it 'does not serialize stage builds' do
- subject.with_indifferent_access.dig(:details, :stages, 0).tap do |stage|
- expect(stage).not_to include(:groups, :latest_statuses, :retries)
- end
- end
-
- context 'user has ability to cancel pipeline' do
- before do
- project.add_developer(user)
- end
-
- it 'contains cancel path' do
- expect(subject[:cancel_path]).to be_present
- end
- end
-
- context 'user does not have ability to cancel pipeline' do
- it 'does not contain cancel path' do
- expect(subject).not_to have_key(:cancel_path)
- end
- end
- end
-
- context 'delete path' do
- context 'user has ability to delete pipeline' do
- let(:project) { create(:project, namespace: user.namespace) }
- let(:pipeline) { create(:ci_pipeline, project: project) }
-
- it 'contains delete path' do
- expect(subject[:delete_path]).to be_present
- end
- end
-
- context 'user does not have ability to delete pipeline' do
- let(:project) { create(:project) }
- let(:pipeline) { create(:ci_pipeline, project: project) }
-
- it 'does not contain delete path' do
- expect(subject).not_to have_key(:delete_path)
- end
- end
- end
-
- context 'when pipeline ref is empty' do
- let(:pipeline) { create(:ci_empty_pipeline) }
-
- before do
- allow(pipeline).to receive(:ref).and_return(nil)
- end
-
- it 'does not generate branch path' do
- expect(subject[:ref][:path]).to be_nil
- end
- end
-
- context 'when pipeline has a failure reason set' do
- let(:pipeline) { create(:ci_empty_pipeline) }
-
- before do
- pipeline.drop!(:config_error)
- end
-
- it 'has a correct failure reason' do
- expect(subject[:failure_reason])
- .to eq 'CI/CD YAML configuration error!'
- end
- end
-
- context 'when pipeline is detached merge request pipeline' do
- let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
- let(:project) { merge_request.target_project }
- let(:pipeline) { merge_request.pipelines_for_merge_request.first }
-
- it 'makes detached flag true' do
- expect(subject[:flags][:detached_merge_request_pipeline]).to be_truthy
- end
-
- it 'does not expose source sha and target sha' do
- expect(subject[:source_sha]).to be_nil
- expect(subject[:target_sha]).to be_nil
- end
-
- context 'when user is a developer' do
- before do
- project.add_developer(user)
- end
-
- it 'has merge request information' do
- expect(subject[:merge_request][:iid]).to eq(merge_request.iid)
-
- expect(project_merge_request_path(project, merge_request))
- .to include(subject[:merge_request][:path])
-
- expect(subject[:merge_request][:title]).to eq(merge_request.title)
-
- expect(subject[:merge_request][:source_branch])
- .to eq(merge_request.source_branch)
-
- expect(project_commits_path(project, merge_request.source_branch))
- .to include(subject[:merge_request][:source_branch_path])
-
- expect(subject[:merge_request][:target_branch])
- .to eq(merge_request.target_branch)
-
- expect(project_commits_path(project, merge_request.target_branch))
- .to include(subject[:merge_request][:target_branch_path])
- end
- end
-
- context 'when user is an external user' do
- it 'has no merge request information' do
- expect(subject[:merge_request]).to be_nil
- end
- end
- end
-
- context 'when pipeline is merge request pipeline' do
- let(:merge_request) { create(:merge_request, :with_merge_request_pipeline, merge_sha: 'abc') }
- let(:project) { merge_request.target_project }
- let(:pipeline) { merge_request.pipelines_for_merge_request.first }
-
- it 'makes detached flag false' do
- expect(subject[:flags][:detached_merge_request_pipeline]).to be_falsy
- end
-
- it 'makes atached flag true' do
- expect(subject[:flags][:merge_request_pipeline]).to be_truthy
- end
-
- it 'exposes source sha and target sha' do
- expect(subject[:source_sha]).to be_present
- expect(subject[:target_sha]).to be_present
- end
-
- it 'exposes merge request event type' do
- expect(subject[:merge_request_event_type]).to be_present
- end
- end
-
- context 'when pipeline has failed builds' do
- let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user) }
- let_it_be(:build) { create(:ci_build, :success, pipeline: pipeline) }
- let_it_be(:failed_1) { create(:ci_build, :failed, pipeline: pipeline) }
- let_it_be(:failed_2) { create(:ci_build, :failed, pipeline: pipeline) }
-
- context 'when the user can retry the pipeline' do
- it 'exposes these failed builds' do
- allow(entity).to receive(:can_retry?).and_return(true)
-
- expect(subject[:failed_builds].map { |b| b[:id] }).to contain_exactly(failed_1.id, failed_2.id)
- end
- end
-
- context 'when the user cannot retry the pipeline' do
- it 'is nil' do
- allow(entity).to receive(:can_retry?).and_return(false)
-
- expect(subject[:failed_builds]).to be_nil
- end
- end
- end
- end
-end
diff --git a/spec/serializers/user_serializer_spec.rb b/spec/serializers/user_serializer_spec.rb
index d54f33b6a23..f2ef1508098 100644
--- a/spec/serializers/user_serializer_spec.rb
+++ b/spec/serializers/user_serializer_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe UserSerializer do
context 'serializer with merge request context' do
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
- let(:serializer) { described_class.new(merge_request_iid: merge_request.iid) }
+ let(:serializer) { described_class.new(current_user: user1, merge_request_iid: merge_request.iid) }
before do
allow(project).to(
diff --git a/spec/services/alert_management/process_prometheus_alert_service_spec.rb b/spec/services/alert_management/process_prometheus_alert_service_spec.rb
index 8f81c1967d5..288a33b71cd 100644
--- a/spec/services/alert_management/process_prometheus_alert_service_spec.rb
+++ b/spec/services/alert_management/process_prometheus_alert_service_spec.rb
@@ -68,36 +68,29 @@ RSpec.describe AlertManagement::ProcessPrometheusAlertService do
let!(:alert) { create(:alert_management_alert, :resolved, project: project, fingerprint: fingerprint) }
it_behaves_like 'creates an alert management alert'
+ it_behaves_like 'Alert Notification Service sends notification email'
end
context 'existing alert is ignored' do
let!(:alert) { create(:alert_management_alert, :ignored, project: project, fingerprint: fingerprint) }
it_behaves_like 'adds an alert management alert event'
+ it_behaves_like 'Alert Notification Service sends no notifications'
end
- context 'two existing alerts, one resolved one open' do
- let!(:resolved_alert) { create(:alert_management_alert, :resolved, project: project, fingerprint: fingerprint) }
- let!(:alert) { create(:alert_management_alert, project: project, fingerprint: fingerprint) }
+ context 'existing alert is acknowledged' do
+ let!(:alert) { create(:alert_management_alert, :acknowledged, project: project, fingerprint: fingerprint) }
it_behaves_like 'adds an alert management alert event'
+ it_behaves_like 'Alert Notification Service sends no notifications'
end
- context 'when status change did not succeed' do
- before do
- allow(AlertManagement::Alert).to receive(:for_fingerprint).and_return([alert])
- allow(alert).to receive(:trigger).and_return(false)
- end
-
- it 'writes a warning to the log' do
- expect(Gitlab::AppLogger).to receive(:warn).with(
- message: 'Unable to update AlertManagement::Alert status to triggered',
- project_id: project.id,
- alert_id: alert.id
- )
+ context 'two existing alerts, one resolved one open' do
+ let!(:resolved_alert) { create(:alert_management_alert, :resolved, project: project, fingerprint: fingerprint) }
+ let!(:alert) { create(:alert_management_alert, project: project, fingerprint: fingerprint) }
- execute
- end
+ it_behaves_like 'adds an alert management alert event'
+ it_behaves_like 'Alert Notification Service sends notification email'
end
context 'when auto-creation of issues is disabled' do
@@ -109,11 +102,7 @@ RSpec.describe AlertManagement::ProcessPrometheusAlertService do
context 'when emails are disabled' do
let(:send_email) { false }
- it 'does not send notification' do
- expect(NotificationService).not_to receive(:new)
-
- expect(subject).to be_success
- end
+ it_behaves_like 'Alert Notification Service sends no notifications'
end
end
@@ -136,11 +125,7 @@ RSpec.describe AlertManagement::ProcessPrometheusAlertService do
context 'when emails are disabled' do
let(:send_email) { false }
- it 'does not send notification' do
- expect(NotificationService).not_to receive(:new)
-
- expect(subject).to be_success
- end
+ it_behaves_like 'Alert Notification Service sends no notifications'
end
end
@@ -158,7 +143,7 @@ RSpec.describe AlertManagement::ProcessPrometheusAlertService do
it 'writes a warning to the log' do
expect(Gitlab::AppLogger).to receive(:warn).with(
- message: 'Unable to create AlertManagement::Alert',
+ message: 'Unable to create AlertManagement::Alert from Prometheus',
project_id: project.id,
alert_errors: { hosts: ['hosts array is over 255 chars'] }
)
@@ -235,11 +220,7 @@ RSpec.describe AlertManagement::ProcessPrometheusAlertService do
context 'when emails are disabled' do
let(:send_email) { false }
- it 'does not send notification' do
- expect(NotificationService).not_to receive(:new)
-
- expect(subject).to be_success
- end
+ it_behaves_like 'Alert Notification Service sends no notifications'
end
end
diff --git a/spec/services/application_settings/update_service_spec.rb b/spec/services/application_settings/update_service_spec.rb
index 46483fede97..1352a595ba4 100644
--- a/spec/services/application_settings/update_service_spec.rb
+++ b/spec/services/application_settings/update_service_spec.rb
@@ -122,7 +122,7 @@ RSpec.describe ApplicationSettings::UpdateService do
it_behaves_like 'invalidates markdown cache', { asset_proxy_enabled: true }
it_behaves_like 'invalidates markdown cache', { asset_proxy_url: 'http://test.com' }
it_behaves_like 'invalidates markdown cache', { asset_proxy_secret_key: 'another secret' }
- it_behaves_like 'invalidates markdown cache', { asset_proxy_whitelist: ['domain.com'] }
+ it_behaves_like 'invalidates markdown cache', { asset_proxy_allowlist: ['domain.com'] }
context 'when also setting the local_markdown_version' do
let(:params) { { asset_proxy_enabled: true, local_markdown_version: 12 } }
diff --git a/spec/services/authorized_project_update/recalculate_for_user_range_service_spec.rb b/spec/services/authorized_project_update/recalculate_for_user_range_service_spec.rb
index a4637b6ba1c..0c944cad40c 100644
--- a/spec/services/authorized_project_update/recalculate_for_user_range_service_spec.rb
+++ b/spec/services/authorized_project_update/recalculate_for_user_range_service_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe AuthorizedProjectUpdate::RecalculateForUserRangeService do
it 'calls Users::RefreshAuthorizedProjectsService' do
users.each do |user|
expect(Users::RefreshAuthorizedProjectsService).to(
- receive(:new).with(user).and_call_original)
+ receive(:new).with(user, source: described_class.name).and_call_original)
end
range = users.map(&:id).minmax
diff --git a/spec/services/boards/lists/create_service_spec.rb b/spec/services/boards/lists/create_service_spec.rb
index d639fdbb46a..cac26b3c88d 100644
--- a/spec/services/boards/lists/create_service_spec.rb
+++ b/spec/services/boards/lists/create_service_spec.rb
@@ -3,99 +3,23 @@
require 'spec_helper'
RSpec.describe Boards::Lists::CreateService do
- describe '#execute' do
- shared_examples 'creating board lists' do
- let_it_be(:user) { create(:user) }
+ context 'when board parent is a project' do
+ let_it_be(:parent) { create(:project) }
+ let_it_be(:board) { create(:board, project: parent) }
+ let_it_be(:label) { create(:label, project: parent, name: 'in-progress') }
- before_all do
- parent.add_developer(user)
- end
-
- subject(:service) { described_class.new(parent, user, label_id: label.id) }
-
- context 'when board lists is empty' do
- it 'creates a new list at beginning of the list' do
- response = service.execute(board)
-
- expect(response.success?).to eq(true)
- expect(response.payload[:list].position).to eq 0
- end
- end
-
- context 'when board lists has the done list' do
- it 'creates a new list at beginning of the list' do
- response = service.execute(board)
-
- expect(response.success?).to eq(true)
- expect(response.payload[:list].position).to eq 0
- end
- end
-
- context 'when board lists has labels lists' do
- it 'creates a new list at end of the lists' do
- create(:list, board: board, position: 0)
- create(:list, board: board, position: 1)
-
- response = service.execute(board)
-
- expect(response.success?).to eq(true)
- expect(response.payload[:list].position).to eq 2
- end
- end
-
- context 'when board lists has label and done lists' do
- it 'creates a new list at end of the label lists' do
- list1 = create(:list, board: board, position: 0)
-
- list2 = service.execute(board).payload[:list]
-
- expect(list1.reload.position).to eq 0
- expect(list2.reload.position).to eq 1
- end
- end
-
- context 'when provided label does not belong to the parent' do
- it 'returns an error' do
- label = create(:label, name: 'in-development')
- service = described_class.new(parent, user, label_id: label.id)
-
- response = service.execute(board)
-
- expect(response.success?).to eq(false)
- expect(response.errors).to include('Label not found')
- end
- end
-
- context 'when backlog param is sent' do
- it 'creates one and only one backlog list' do
- service = described_class.new(parent, user, 'backlog' => true)
- list = service.execute(board).payload[:list]
-
- expect(list.list_type).to eq('backlog')
- expect(list.position).to be_nil
- expect(list).to be_valid
-
- another_backlog = service.execute(board).payload[:list]
-
- expect(another_backlog).to eq list
- end
- end
- end
-
- context 'when board parent is a project' do
- let_it_be(:parent) { create(:project) }
- let_it_be(:board) { create(:board, project: parent) }
- let_it_be(:label) { create(:label, project: parent, name: 'in-progress') }
+ it_behaves_like 'board lists create service'
+ end
- it_behaves_like 'creating board lists'
- end
+ context 'when board parent is a group' do
+ let_it_be(:parent) { create(:group) }
+ let_it_be(:board) { create(:board, group: parent) }
+ let_it_be(:label) { create(:group_label, group: parent, name: 'in-progress') }
- context 'when board parent is a group' do
- let_it_be(:parent) { create(:group) }
- let_it_be(:board) { create(:board, group: parent) }
- let_it_be(:label) { create(:group_label, group: parent, name: 'in-progress') }
+ it_behaves_like 'board lists create service'
+ end
- it_behaves_like 'creating board lists'
- end
+ def create_list(params)
+ create(:list, params.merge(board: board))
end
end
diff --git a/spec/services/bulk_create_integration_service_spec.rb b/spec/services/bulk_create_integration_service_spec.rb
index 674382ee14f..3ac993972c6 100644
--- a/spec/services/bulk_create_integration_service_spec.rb
+++ b/spec/services/bulk_create_integration_service_spec.rb
@@ -43,46 +43,6 @@ RSpec.describe BulkCreateIntegrationService do
end
end
- shared_examples 'updates project callbacks' do
- it 'updates projects#has_external_issue_tracker for issue tracker services' do
- described_class.new(integration, batch, association).execute
-
- expect(project.reload.has_external_issue_tracker).to eq(true)
- expect(excluded_project.reload.has_external_issue_tracker).to eq(false)
- end
-
- context 'with an external wiki integration' do
- before do
- integration.update!(category: 'common', type: 'ExternalWikiService')
- end
-
- it 'updates projects#has_external_wiki for external wiki services' do
- described_class.new(integration, batch, association).execute
-
- expect(project.reload.has_external_wiki).to eq(true)
- expect(excluded_project.reload.has_external_wiki).to eq(false)
- end
- end
- end
-
- shared_examples 'does not update project callbacks' do
- it 'does not update projects#has_external_issue_tracker for issue tracker services' do
- described_class.new(integration, batch, association).execute
-
- expect(project.reload.has_external_issue_tracker).to eq(false)
- end
-
- context 'with an inactive external wiki integration' do
- let(:integration) { create(:external_wiki_service, :instance, active: false) }
-
- it 'does not update projects#has_external_wiki for external wiki services' do
- described_class.new(integration, batch, association).execute
-
- expect(project.reload.has_external_wiki).to eq(false)
- end
- end
- end
-
context 'passing an instance-level integration' do
let(:integration) { instance_integration }
let(:inherit_from_id) { integration.id }
@@ -95,15 +55,6 @@ RSpec.describe BulkCreateIntegrationService do
it_behaves_like 'creates integration from batch ids'
it_behaves_like 'updates inherit_from_id'
- it_behaves_like 'updates project callbacks'
-
- context 'when integration is not active' do
- before do
- integration.update!(active: false)
- end
-
- it_behaves_like 'does not update project callbacks'
- end
end
context 'with a group association' do
@@ -130,7 +81,6 @@ RSpec.describe BulkCreateIntegrationService do
it_behaves_like 'creates integration from batch ids'
it_behaves_like 'updates inherit_from_id'
- it_behaves_like 'updates project callbacks'
end
context 'with a group association' do
@@ -157,7 +107,6 @@ RSpec.describe BulkCreateIntegrationService do
let(:inherit_from_id) { integration.id }
it_behaves_like 'creates integration from batch ids'
- it_behaves_like 'updates project callbacks'
end
end
end
diff --git a/spec/services/captcha/captcha_verification_service_spec.rb b/spec/services/captcha/captcha_verification_service_spec.rb
new file mode 100644
index 00000000000..245e06703f5
--- /dev/null
+++ b/spec/services/captcha/captcha_verification_service_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Captcha::CaptchaVerificationService do
+ describe '#execute' do
+ let(:captcha_response) { nil }
+ let(:request) { double(:request) }
+ let(:service) { described_class.new }
+
+ subject { service.execute(captcha_response: captcha_response, request: request) }
+
+ context 'when there is no captcha_response' do
+ it 'returns false' do
+ expect(subject).to eq(false)
+ end
+ end
+
+ context 'when there is a captcha_response' do
+ let(:captcha_response) { 'abc123' }
+
+ before do
+ expect(Gitlab::Recaptcha).to receive(:load_configurations!)
+ end
+
+ it 'returns false' do
+ expect(service).to receive(:verify_recaptcha).with(response: captcha_response) { true }
+
+ expect(subject).to eq(true)
+ end
+
+ it 'has a request method which returns the request' do
+ subject
+
+ expect(service.send(:request)).to eq(request)
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/create_job_artifacts_service_spec.rb b/spec/services/ci/create_job_artifacts_service_spec.rb
index 29e51a23dea..1efd1d390a2 100644
--- a/spec/services/ci/create_job_artifacts_service_spec.rb
+++ b/spec/services/ci/create_job_artifacts_service_spec.rb
@@ -27,6 +27,14 @@ RSpec.describe Ci::CreateJobArtifactsService do
UploadedFile.new(upload.path, **params)
end
+ def unique_metrics_report_uploaders
+ Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(
+ event_names: described_class::METRICS_REPORT_UPLOAD_EVENT_NAME,
+ start_date: 2.weeks.ago,
+ end_date: 2.weeks.from_now
+ )
+ end
+
describe '#execute' do
subject { service.execute(artifacts_file, params, metadata_file: metadata_file) }
@@ -42,6 +50,12 @@ RSpec.describe Ci::CreateJobArtifactsService do
expect(new_artifact.file_sha256).to eq(artifacts_sha256)
end
+ it 'does not track the job user_id' do
+ subject
+
+ expect(unique_metrics_report_uploaders).to eq(0)
+ end
+
context 'when metadata file is also uploaded' do
let(:metadata_file) do
file_to_upload('spec/fixtures/ci_build_artifacts_metadata.gz', sha256: artifacts_sha256)
@@ -174,6 +188,20 @@ RSpec.describe Ci::CreateJobArtifactsService do
end
end
+ context 'when artifact_type is metrics' do
+ before do
+ allow(job).to receive(:user_id).and_return(123)
+ end
+
+ let(:params) { { 'artifact_type' => 'metrics', 'artifact_format' => 'gzip' }.with_indifferent_access }
+
+ it 'tracks the job user_id' do
+ subject
+
+ expect(unique_metrics_report_uploaders).to eq(1)
+ end
+ end
+
context 'when artifact type is cluster_applications' do
let(:artifacts_file) do
file_to_upload('spec/fixtures/helm/helm_list_v2_prometheus_missing.json.gz', sha256: artifacts_sha256)
diff --git a/spec/services/ci/create_pipeline_service/cross_project_pipeline_spec.rb b/spec/services/ci/create_pipeline_service/cross_project_pipeline_spec.rb
new file mode 100644
index 00000000000..9cf66dfceb0
--- /dev/null
+++ b/spec/services/ci/create_pipeline_service/cross_project_pipeline_spec.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::CreatePipelineService, '#execute' do
+ let_it_be(:group) { create(:group, name: 'my-organization') }
+ let(:upstream_project) { create(:project, :repository, name: 'upstream', group: group) }
+ let(:downstram_project) { create(:project, :repository, name: 'downstream', group: group) }
+ let(:user) { create(:user) }
+
+ let(:service) do
+ described_class.new(upstream_project, user, ref: 'master')
+ end
+
+ before do
+ upstream_project.add_developer(user)
+ downstram_project.add_developer(user)
+ create_gitlab_ci_yml(upstream_project, upstream_config)
+ create_gitlab_ci_yml(downstram_project, downstream_config)
+ end
+
+ context 'with resource group', :aggregate_failures do
+ let(:upstream_config) do
+ <<~YAML
+ instrumentation_test:
+ stage: test
+ resource_group: iOS
+ trigger:
+ project: my-organization/downstream
+ strategy: depend
+ YAML
+ end
+
+ let(:downstream_config) do
+ <<~YAML
+ test:
+ script: echo "Testing..."
+ YAML
+ end
+
+ it 'creates bridge job with resource group' do
+ pipeline = create_pipeline!
+
+ test = pipeline.statuses.find_by(name: 'instrumentation_test')
+ expect(pipeline).to be_created_successfully
+ expect(pipeline.triggered_pipelines).not_to be_exist
+ expect(upstream_project.resource_groups.count).to eq(1)
+ expect(test).to be_a Ci::Bridge
+ expect(test).to be_waiting_for_resource
+ expect(test.resource_group.key).to eq('iOS')
+ end
+
+ context 'when sidekiq processes the job', :sidekiq_inline do
+ it 'transitions to pending status and triggers a downstream pipeline' do
+ pipeline = create_pipeline!
+
+ test = pipeline.statuses.find_by(name: 'instrumentation_test')
+ expect(test).to be_pending
+ expect(pipeline.triggered_pipelines.count).to eq(1)
+ end
+
+ context 'when the resource is occupied by the other bridge' do
+ before do
+ resource_group = create(:ci_resource_group, project: upstream_project, key: 'iOS')
+ resource_group.assign_resource_to(create(:ci_build, project: upstream_project))
+ end
+
+ it 'stays waiting for resource' do
+ pipeline = create_pipeline!
+
+ test = pipeline.statuses.find_by(name: 'instrumentation_test')
+ expect(test).to be_waiting_for_resource
+ expect(pipeline.triggered_pipelines.count).to eq(0)
+ end
+ end
+ end
+ end
+
+ def create_pipeline!
+ service.execute(:push)
+ end
+
+ def create_gitlab_ci_yml(project, content)
+ project.repository.create_file(user, '.gitlab-ci.yml', content, branch_name: 'master', message: 'test')
+ end
+end
diff --git a/spec/services/ci/create_pipeline_service/custom_yaml_tags_spec.rb b/spec/services/ci/create_pipeline_service/custom_yaml_tags_spec.rb
new file mode 100644
index 00000000000..4cf52223e38
--- /dev/null
+++ b/spec/services/ci/create_pipeline_service/custom_yaml_tags_spec.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Ci::CreatePipelineService do
+ describe '!reference tags' do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { project.owner }
+
+ let(:ref) { 'refs/heads/master' }
+ let(:source) { :push }
+ let(:service) { described_class.new(project, user, { ref: ref }) }
+ let(:pipeline) { service.execute(source) }
+
+ before do
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ context 'with valid config' do
+ let(:config) do
+ <<~YAML
+ .job-1:
+ script:
+ - echo doing step 1 of job 1
+
+ .job-2:
+ before_script:
+ - ls
+ script: !reference [.job-1, script]
+
+ job:
+ before_script: !reference [.job-2, before_script]
+ script:
+ - echo doing my first step
+ - !reference [.job-2, script]
+ - echo doing my last step
+ YAML
+ end
+
+ it 'creates a pipeline' do
+ expect(pipeline).to be_persisted
+ expect(pipeline.builds.first.options).to match(a_hash_including({
+ 'before_script' => ['ls'],
+ 'script' => [
+ 'echo doing my first step',
+ 'echo doing step 1 of job 1',
+ 'echo doing my last step'
+ ]
+ }))
+ end
+ end
+
+ context 'with invalid config' do
+ let(:config) do
+ <<~YAML
+ job-1:
+ script:
+ - echo doing step 1 of job 1
+ - !reference [job-3, script]
+
+ job-2:
+ script:
+ - echo doing step 1 of job 2
+ - !reference [job-3, script]
+
+ job-3:
+ script:
+ - echo doing step 1 of job 3
+ - !reference [job-1, script]
+ YAML
+ end
+
+ it 'creates a pipeline without builds' do
+ expect(pipeline).to be_persisted
+ expect(pipeline.builds).to be_empty
+ expect(pipeline.yaml_errors).to eq("!reference [\"job-3\", \"script\"] is part of a circular chain")
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb b/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb
index 8df9b0c3e60..a3818937113 100644
--- a/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb
+++ b/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb
@@ -76,6 +76,56 @@ RSpec.describe Ci::CreatePipelineService, '#execute' do
}
end
end
+
+ context 'with resource group' do
+ let(:config) do
+ <<~YAML
+ instrumentation_test:
+ stage: test
+ resource_group: iOS
+ trigger:
+ include: path/to/child.yml
+ strategy: depend
+ YAML
+ end
+
+ it 'creates bridge job with resource group', :aggregate_failures do
+ pipeline = create_pipeline!
+
+ test = pipeline.statuses.find_by(name: 'instrumentation_test')
+ expect(pipeline).to be_created_successfully
+ expect(pipeline.triggered_pipelines).not_to be_exist
+ expect(project.resource_groups.count).to eq(1)
+ expect(test).to be_a Ci::Bridge
+ expect(test).to be_waiting_for_resource
+ expect(test.resource_group.key).to eq('iOS')
+ end
+
+ context 'when sidekiq processes the job', :sidekiq_inline do
+ it 'transitions to pending status and triggers a downstream pipeline' do
+ pipeline = create_pipeline!
+
+ test = pipeline.statuses.find_by(name: 'instrumentation_test')
+ expect(test).to be_pending
+ expect(pipeline.triggered_pipelines.count).to eq(1)
+ end
+
+ context 'when the resource is occupied by the other bridge' do
+ before do
+ resource_group = create(:ci_resource_group, project: project, key: 'iOS')
+ resource_group.assign_resource_to(create(:ci_build, project: project))
+ end
+
+ it 'stays waiting for resource' do
+ pipeline = create_pipeline!
+
+ test = pipeline.statuses.find_by(name: 'instrumentation_test')
+ expect(test).to be_waiting_for_resource
+ expect(pipeline.triggered_pipelines.count).to eq(0)
+ end
+ end
+ end
+ end
end
describe 'child pipeline triggers' do
diff --git a/spec/services/ci/create_pipeline_service/rules_spec.rb b/spec/services/ci/create_pipeline_service/rules_spec.rb
index ac6c4c188e4..04ecac6a85a 100644
--- a/spec/services/ci/create_pipeline_service/rules_spec.rb
+++ b/spec/services/ci/create_pipeline_service/rules_spec.rb
@@ -145,20 +145,6 @@ RSpec.describe Ci::CreatePipelineService do
expect(find_job('job-2').options.dig(:allow_failure_criteria)).to be_nil
expect(find_job('job-3').options.dig(:allow_failure_criteria, :exit_codes)).to eq([42])
end
-
- context 'with ci_allow_failure_with_exit_codes disabled' do
- before do
- stub_feature_flags(ci_allow_failure_with_exit_codes: false)
- end
-
- it 'does not persist allow_failure_criteria' do
- expect(pipeline).to be_persisted
-
- expect(find_job('job-1').options.key?(:allow_failure_criteria)).to be_falsey
- expect(find_job('job-2').options.key?(:allow_failure_criteria)).to be_falsey
- expect(find_job('job-3').options.key?(:allow_failure_criteria)).to be_falsey
- end
- end
end
context 'if:' do
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index e1f1bdc41a1..1005985b3e4 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -102,7 +102,6 @@ RSpec.describe Ci::CreatePipelineService do
describe 'recording a conversion event' do
it 'schedules a record conversion event worker' do
expect(Experiments::RecordConversionEventWorker).to receive(:perform_async).with(:ci_syntax_templates, user.id)
- expect(Experiments::RecordConversionEventWorker).to receive(:perform_async).with(:pipelines_empty_state, user.id)
pipeline
end
@@ -538,7 +537,7 @@ RSpec.describe Ci::CreatePipelineService do
it 'pull it from Auto-DevOps' do
pipeline = execute_service
expect(pipeline).to be_auto_devops_source
- expect(pipeline.builds.map(&:name)).to match_array(%w[build code_quality eslint-sast secret_detection_default_branch test])
+ expect(pipeline.builds.map(&:name)).to match_array(%w[brakeman-sast build code_quality eslint-sast secret_detection_default_branch test])
end
end
@@ -952,9 +951,9 @@ RSpec.describe Ci::CreatePipelineService do
expect(result).to be_persisted
expect(deploy_job.resource_group.key).to eq(resource_group_key)
expect(project.resource_groups.count).to eq(1)
- expect(resource_group.builds.count).to eq(1)
+ expect(resource_group.processables.count).to eq(1)
expect(resource_group.resources.count).to eq(1)
- expect(resource_group.resources.first.build).to eq(nil)
+ expect(resource_group.resources.first.processable).to eq(nil)
end
context 'when resource group key includes predefined variables' do
diff --git a/spec/services/ci/daily_build_group_report_result_service_spec.rb b/spec/services/ci/daily_build_group_report_result_service_spec.rb
index e54f10cc4f4..e58a5de26a1 100644
--- a/spec/services/ci/daily_build_group_report_result_service_spec.rb
+++ b/spec/services/ci/daily_build_group_report_result_service_spec.rb
@@ -3,10 +3,12 @@
require 'spec_helper'
RSpec.describe Ci::DailyBuildGroupReportResultService, '#execute' do
- let!(:pipeline) { create(:ci_pipeline, created_at: '2020-02-06 00:01:10') }
- let!(:rspec_job) { create(:ci_build, pipeline: pipeline, name: '3/3 rspec', coverage: 80) }
- let!(:karma_job) { create(:ci_build, pipeline: pipeline, name: '2/2 karma', coverage: 90) }
- let!(:extra_job) { create(:ci_build, pipeline: pipeline, name: 'extra', coverage: nil) }
+ let_it_be(:group) { create(:group, :private) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: create(:project, group: group), created_at: '2020-02-06 00:01:10') }
+ let_it_be(:rspec_job) { create(:ci_build, pipeline: pipeline, name: 'rspec 3/3', coverage: 80) }
+ let_it_be(:karma_job) { create(:ci_build, pipeline: pipeline, name: 'karma 2/2', coverage: 90) }
+ let_it_be(:extra_job) { create(:ci_build, pipeline: pipeline, name: 'extra', coverage: nil) }
+
let(:coverages) { Ci::DailyBuildGroupReportResult.all }
it 'creates daily code coverage record for each job in the pipeline that has coverage value' do
@@ -19,7 +21,8 @@ RSpec.describe Ci::DailyBuildGroupReportResultService, '#execute' do
ref_path: pipeline.source_ref_path,
group_name: rspec_job.group_name,
data: { 'coverage' => rspec_job.coverage },
- date: pipeline.created_at.to_date
+ date: pipeline.created_at.to_date,
+ group_id: group.id
)
end
@@ -30,7 +33,8 @@ RSpec.describe Ci::DailyBuildGroupReportResultService, '#execute' do
ref_path: pipeline.source_ref_path,
group_name: karma_job.group_name,
data: { 'coverage' => karma_job.coverage },
- date: pipeline.created_at.to_date
+ date: pipeline.created_at.to_date,
+ group_id: group.id
)
end
@@ -38,8 +42,8 @@ RSpec.describe Ci::DailyBuildGroupReportResultService, '#execute' do
end
context 'when there are multiple builds with the same group name that report coverage' do
- let!(:test_job_1) { create(:ci_build, pipeline: pipeline, name: '1/2 test', coverage: 70) }
- let!(:test_job_2) { create(:ci_build, pipeline: pipeline, name: '2/2 test', coverage: 80) }
+ let!(:test_job_1) { create(:ci_build, pipeline: pipeline, name: 'test 1/2', coverage: 70) }
+ let!(:test_job_2) { create(:ci_build, pipeline: pipeline, name: 'test 2/2', coverage: 80) }
it 'creates daily code coverage record with the average as the value' do
described_class.new.execute(pipeline)
@@ -67,8 +71,8 @@ RSpec.describe Ci::DailyBuildGroupReportResultService, '#execute' do
)
end
- let!(:new_rspec_job) { create(:ci_build, pipeline: new_pipeline, name: '4/4 rspec', coverage: 84) }
- let!(:new_karma_job) { create(:ci_build, pipeline: new_pipeline, name: '3/3 karma', coverage: 92) }
+ let!(:new_rspec_job) { create(:ci_build, pipeline: new_pipeline, name: 'rspec 4/4', coverage: 84) }
+ let!(:new_karma_job) { create(:ci_build, pipeline: new_pipeline, name: 'karma 3/3', coverage: 92) }
before do
# Create the existing daily code coverage records
@@ -107,8 +111,8 @@ RSpec.describe Ci::DailyBuildGroupReportResultService, '#execute' do
)
end
- let!(:new_rspec_job) { create(:ci_build, pipeline: new_pipeline, name: '4/4 rspec', coverage: 84) }
- let!(:new_karma_job) { create(:ci_build, pipeline: new_pipeline, name: '3/3 karma', coverage: 92) }
+ let!(:new_rspec_job) { create(:ci_build, pipeline: new_pipeline, name: 'rspec 4/4', coverage: 84) }
+ let!(:new_karma_job) { create(:ci_build, pipeline: new_pipeline, name: 'karma 3/3', coverage: 92) }
before do
# Create the existing daily code coverage records
diff --git a/spec/services/ci/generate_codequality_mr_diff_report_service_spec.rb b/spec/services/ci/generate_codequality_mr_diff_report_service_spec.rb
new file mode 100644
index 00000000000..5d747a09f2a
--- /dev/null
+++ b/spec/services/ci/generate_codequality_mr_diff_report_service_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::GenerateCodequalityMrDiffReportService do
+ let(:service) { described_class.new(project) }
+ let(:project) { create(:project, :repository) }
+
+ describe '#execute' do
+ subject { service.execute(base_pipeline, head_pipeline) }
+
+ context 'when head pipeline has codequality mr diff report' do
+ let!(:merge_request) { create(:merge_request, :with_codequality_mr_diff_reports, source_project: project) }
+ let!(:service) { described_class.new(project, nil, id: merge_request.id) }
+ let!(:head_pipeline) { merge_request.head_pipeline }
+ let!(:base_pipeline) { nil }
+
+ it 'returns status and data', :aggregate_failures do
+ expect_any_instance_of(Ci::PipelineArtifact) do |instance|
+ expect(instance).to receive(:present)
+ expect(instance).to receive(:for_files).with(merge_request.new_paths).and_call_original
+ end
+
+ expect(subject[:status]).to eq(:parsed)
+ expect(subject[:data]).to eq(files: {})
+ end
+ end
+
+ context 'when head pipeline does not have a codequality mr diff report' do
+ let!(:merge_request) { create(:merge_request, source_project: project) }
+ let!(:service) { described_class.new(project, nil, id: merge_request.id) }
+ let!(:head_pipeline) { merge_request.head_pipeline }
+ let!(:base_pipeline) { nil }
+
+ it 'returns status and error message' do
+ expect(subject[:status]).to eq(:error)
+ expect(subject[:status_reason]).to include('An error occurred while fetching codequality mr diff reports.')
+ end
+ end
+
+ context 'when head pipeline has codequality mr diff report and no merge request associated' do
+ let!(:head_pipeline) { create(:ci_pipeline, :with_codequality_mr_diff_report, project: project) }
+ let!(:base_pipeline) { nil }
+
+ it 'returns status and error message' do
+ expect(subject[:status]).to eq(:error)
+ expect(subject[:status_reason]).to include('An error occurred while fetching codequality mr diff reports.')
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service_spec.rb b/spec/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service_spec.rb
new file mode 100644
index 00000000000..0c48f15d726
--- /dev/null
+++ b/spec/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Ci::PipelineArtifacts::CreateCodeQualityMrDiffReportService do
+ describe '#execute' do
+ subject(:pipeline_artifact) { described_class.new.execute(pipeline) }
+
+ context 'when pipeline has codequality reports' do
+ let(:project) { create(:project, :repository) }
+
+ describe 'pipeline completed status' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:status, :result) do
+ :success | 1
+ :failed | 1
+ :canceled | 1
+ :skipped | 1
+ end
+
+ with_them do
+ let(:pipeline) { create(:ci_pipeline, :with_codequality_reports, status: status, project: project) }
+
+ it 'creates a pipeline artifact' do
+ expect { pipeline_artifact }.to change(Ci::PipelineArtifact, :count).by(result)
+ end
+
+ it 'persists the default file name' do
+ expect(pipeline_artifact.file.filename).to eq('code_quality_mr_diff.json')
+ end
+
+ it 'sets expire_at to 1 week' do
+ freeze_time do
+ expect(pipeline_artifact.expire_at).to eq(1.week.from_now)
+ end
+ end
+ end
+ end
+
+ context 'when pipeline artifact has already been created' do
+ let(:pipeline) { create(:ci_pipeline, :with_codequality_reports, project: project) }
+
+ it 'does not persist the same artifact twice' do
+ 2.times { described_class.new.execute(pipeline) }
+
+ expect(Ci::PipelineArtifact.count).to eq(1)
+ end
+ end
+ end
+
+ context 'when pipeline is not completed and codequality report does not exist' do
+ let(:pipeline) { create(:ci_pipeline, :running) }
+
+ it 'does not persist data' do
+ pipeline_artifact
+
+ expect(Ci::PipelineArtifact.count).to eq(0)
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/pipeline_trigger_service_spec.rb b/spec/services/ci/pipeline_trigger_service_spec.rb
index 0cc66e67b91..89d3da89011 100644
--- a/spec/services/ci/pipeline_trigger_service_spec.rb
+++ b/spec/services/ci/pipeline_trigger_service_spec.rb
@@ -45,6 +45,27 @@ RSpec.describe Ci::PipelineTriggerService do
expect(result[:status]).to eq(:success)
end
+ it 'stores the payload as a variable' do
+ expect { result }.to change { Ci::PipelineVariable.count }.by(1)
+
+ var = result[:pipeline].variables.first
+
+ expect(var.key).to eq('TRIGGER_PAYLOAD')
+ expect(var.value).to eq('{"ref":"master","variables":null}')
+ expect(var.variable_type).to eq('file')
+ end
+
+ context 'when FF ci_trigger_payload_into_pipeline is disabled' do
+ before do
+ stub_feature_flags(ci_trigger_payload_into_pipeline: false)
+ end
+
+ it 'does not store the payload as a variable' do
+ expect { result }.not_to change { Ci::PipelineVariable.count }
+ expect(result[:pipeline].variables).to be_empty
+ end
+ end
+
context 'when commit message has [ci skip]' do
before do
allow_next(Ci::Pipeline).to receive(:git_commit_message) { '[ci skip]' }
@@ -60,8 +81,8 @@ RSpec.describe Ci::PipelineTriggerService do
let(:params) { { token: trigger.token, ref: 'master', variables: variables } }
let(:variables) { { 'AAA' => 'AAA123' } }
- it 'has a variable' do
- expect { result }.to change { Ci::PipelineVariable.count }.by(1)
+ it 'has variables' do
+ expect { result }.to change { Ci::PipelineVariable.count }.by(2)
.and change { Ci::TriggerRequest.count }.by(1)
expect(result[:pipeline].variables.map { |v| { v.key => v.value } }.first).to eq(variables)
expect(result[:pipeline].trigger_requests.last.variables).to be_nil
@@ -155,8 +176,8 @@ RSpec.describe Ci::PipelineTriggerService do
let(:params) { { token: job.token, ref: 'master', variables: variables } }
let(:variables) { { 'AAA' => 'AAA123' } }
- it 'has a variable' do
- expect { result }.to change { Ci::PipelineVariable.count }.by(1)
+ it 'has variables' do
+ expect { result }.to change { Ci::PipelineVariable.count }.by(2)
.and change { Ci::Sources::Pipeline.count }.by(1)
expect(result[:pipeline].variables.map { |v| { v.key => v.value } }.first).to eq(variables)
expect(job.sourced_pipelines.last.pipeline_id).to eq(result[:pipeline].id)
diff --git a/spec/services/ci/process_build_service_spec.rb b/spec/services/ci/process_build_service_spec.rb
index 6d2af81a6e8..42a92504839 100644
--- a/spec/services/ci/process_build_service_spec.rb
+++ b/spec/services/ci/process_build_service_spec.rb
@@ -146,9 +146,11 @@ RSpec.describe Ci::ProcessBuildService, '#execute' do
end
end
- context 'when FF skip_dag_manual_and_delayed_jobs is disabled' do
+ context 'when FF skip_dag_manual_and_delayed_jobs is disabled on the project' do
+ let_it_be(:other_project) { create(:project) }
+
before do
- stub_feature_flags(skip_dag_manual_and_delayed_jobs: false)
+ stub_feature_flags(skip_dag_manual_and_delayed_jobs: other_project)
end
where(:build_when, :current_status, :after_status) do
diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb
index a7889f0644d..d316c9a262b 100644
--- a/spec/services/ci/process_pipeline_service_spec.rb
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -50,6 +50,35 @@ RSpec.describe Ci::ProcessPipelineService do
expect(all_builds.retried).to contain_exactly(build_retried)
end
+ context 'counter ci_legacy_update_jobs_as_retried_total' do
+ let(:counter) { double(increment: true) }
+
+ before do
+ allow(Gitlab::Metrics).to receive(:counter).and_call_original
+ allow(Gitlab::Metrics).to receive(:counter)
+ .with(:ci_legacy_update_jobs_as_retried_total, anything)
+ .and_return(counter)
+ end
+
+ it 'increments the counter' do
+ expect(counter).to receive(:increment)
+
+ subject.execute
+ end
+
+ context 'when the previous build has already retried column true' do
+ before do
+ build_retried.update_columns(retried: true)
+ end
+
+ it 'does not increment the counter' do
+ expect(counter).not_to receive(:increment)
+
+ subject.execute
+ end
+ end
+ end
+
def create_build(name, **opts)
create(:ci_build, :created, pipeline: pipeline, name: name, **opts)
end
diff --git a/spec/services/ci/prometheus_metrics/observe_histograms_service_spec.rb b/spec/services/ci/prometheus_metrics/observe_histograms_service_spec.rb
new file mode 100644
index 00000000000..2eef852b0f4
--- /dev/null
+++ b/spec/services/ci/prometheus_metrics/observe_histograms_service_spec.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::PrometheusMetrics::ObserveHistogramsService do
+ let_it_be(:project) { create(:project) }
+ let(:params) { {} }
+
+ subject(:execute) { described_class.new(project, params).execute }
+
+ before do
+ Gitlab::Metrics.reset_registry!
+ end
+
+ context 'with empty data' do
+ it 'does not raise errors' do
+ is_expected.to be_success
+ end
+ end
+
+ context 'observes metrics successfully' do
+ let(:params) do
+ {
+ histograms: [
+ { name: 'pipeline_graph_link_calculation_duration_seconds', value: '1' },
+ { name: 'pipeline_graph_links_per_job_ratio', value: '0.9' }
+ ]
+ }
+ end
+
+ it 'increments the metrics' do
+ execute
+
+ expect(histogram_data).to match(a_hash_including({ 0.8 => 0.0, 1 => 1.0, 2 => 1.0 }))
+
+ expect(histogram_data(:pipeline_graph_links_per_job_ratio))
+ .to match(a_hash_including({ 0.8 => 0.0, 0.9 => 1.0, 1 => 1.0 }))
+ end
+
+ it 'returns an empty body and status code' do
+ is_expected.to be_success
+ expect(subject.http_status).to eq(:created)
+ expect(subject.payload).to eq({})
+ end
+ end
+
+ context 'with unknown histograms' do
+ let(:params) do
+ { histograms: [{ name: 'chunky_bacon', value: '4' }] }
+ end
+
+ it 'raises ActiveRecord::RecordNotFound error' do
+ expect { subject }.to raise_error ActiveRecord::RecordNotFound
+ end
+ end
+
+ context 'with feature flag disabled' do
+ before do
+ stub_feature_flags(ci_accept_frontend_prometheus_metrics: false)
+ end
+
+ let(:params) do
+ {
+ histograms: [
+ { name: 'pipeline_graph_link_calculation_duration_seconds', value: '4' }
+ ]
+ }
+ end
+
+ it 'does not register the metrics' do
+ execute
+
+ expect(histogram_data).to be_nil
+ end
+
+ it 'returns an empty body and status code' do
+ is_expected.to be_success
+ expect(subject.http_status).to eq(:accepted)
+ expect(subject.payload).to eq({})
+ end
+ end
+
+ def histogram_data(name = :pipeline_graph_link_calculation_duration_seconds)
+ Gitlab::Metrics.registry.get(name)&.get({})
+ end
+end
diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb
index 0cdc8d2c870..88770c8095b 100644
--- a/spec/services/ci/register_job_service_spec.rb
+++ b/spec/services/ci/register_job_service_spec.rb
@@ -455,7 +455,7 @@ module Ci
end
before do
- stub_feature_flags(ci_disable_validates_dependencies: false)
+ stub_feature_flags(ci_validate_build_dependencies_override: false)
end
let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0) }
@@ -470,15 +470,31 @@ module Ci
context 'when validates for dependencies is enabled' do
before do
- stub_feature_flags(ci_disable_validates_dependencies: false)
+ stub_feature_flags(ci_validate_build_dependencies_override: false)
end
it_behaves_like 'validation is active'
+
+ context 'when the main feature flag is enabled for a specific project' do
+ before do
+ stub_feature_flags(ci_validate_build_dependencies: pipeline.project)
+ end
+
+ it_behaves_like 'validation is active'
+ end
+
+ context 'when the main feature flag is enabled for a different project' do
+ before do
+ stub_feature_flags(ci_validate_build_dependencies: create(:project))
+ end
+
+ it_behaves_like 'validation is not active'
+ end
end
context 'when validates for dependencies is disabled' do
before do
- stub_feature_flags(ci_disable_validates_dependencies: true)
+ stub_feature_flags(ci_validate_build_dependencies_override: true)
end
it_behaves_like 'validation is not active'
diff --git a/spec/services/container_expiration_policies/cleanup_service_spec.rb b/spec/services/container_expiration_policies/cleanup_service_spec.rb
index 34f69d24141..746e3464427 100644
--- a/spec/services/container_expiration_policies/cleanup_service_spec.rb
+++ b/spec/services/container_expiration_policies/cleanup_service_spec.rb
@@ -61,7 +61,8 @@ RSpec.describe ContainerExpirationPolicies::CleanupService do
original_size: 1000,
before_truncate_size: 800,
after_truncate_size: 200,
- before_delete_size: 100
+ before_delete_size: 100,
+ deleted_size: 100
}
end
@@ -77,7 +78,8 @@ RSpec.describe ContainerExpirationPolicies::CleanupService do
cleanup_tags_service_original_size: 1000,
cleanup_tags_service_before_truncate_size: 800,
cleanup_tags_service_after_truncate_size: 200,
- cleanup_tags_service_before_delete_size: 100
+ cleanup_tags_service_before_delete_size: 100,
+ cleanup_tags_service_deleted_size: 100
)
expect(ContainerRepository.waiting_for_cleanup.count).to eq(1)
expect(repository.reload.cleanup_unfinished?).to be_truthy
@@ -97,5 +99,21 @@ RSpec.describe ContainerExpirationPolicies::CleanupService do
expect(response.success?).to eq(false)
end
end
+
+ context 'with a network error' do
+ before do
+ expect(Projects::ContainerRepository::CleanupTagsService)
+ .to receive(:new).and_raise(Faraday::TimeoutError)
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(Faraday::TimeoutError)
+
+ expect(ContainerRepository.waiting_for_cleanup.count).to eq(1)
+ expect(repository.reload.cleanup_unfinished?).to be_truthy
+ expect(repository.expiration_policy_started_at).not_to eq(nil)
+ expect(repository.expiration_policy_completed_at).to eq(nil)
+ end
+ end
end
end
diff --git a/spec/services/deployments/create_service_spec.rb b/spec/services/deployments/create_service_spec.rb
index 2d157c9d114..0bb5949ddb1 100644
--- a/spec/services/deployments/create_service_spec.rb
+++ b/spec/services/deployments/create_service_spec.rb
@@ -41,6 +41,27 @@ RSpec.describe Deployments::CreateService do
expect(service.execute).to be_persisted
end
+
+ context 'when the last deployment has the same parameters' do
+ let(:params) do
+ {
+ sha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0',
+ ref: 'master',
+ tag: false,
+ status: 'success'
+ }
+ end
+
+ it 'does not create a new deployment' do
+ described_class.new(environment, user, params).execute
+
+ expect(Deployments::UpdateEnvironmentWorker).not_to receive(:perform_async)
+ expect(Deployments::LinkMergeRequestWorker).not_to receive(:perform_async)
+ expect(Deployments::ExecuteHooksWorker).not_to receive(:perform_async)
+
+ described_class.new(environment.reload, user, params).execute
+ end
+ end
end
describe '#deployment_attributes' do
diff --git a/spec/services/design_management/move_designs_service_spec.rb b/spec/services/design_management/move_designs_service_spec.rb
index a43f0a2f805..c8abce77325 100644
--- a/spec/services/design_management/move_designs_service_spec.rb
+++ b/spec/services/design_management/move_designs_service_spec.rb
@@ -76,18 +76,6 @@ RSpec.describe DesignManagement::MoveDesignsService do
end
end
- context 'the designs are not adjacent' do
- let(:current_design) { designs.first }
- let(:previous_design) { designs.second }
- let(:next_design) { designs.third }
-
- it 'raises not_adjacent' do
- create(:design, issue: issue, relative_position: next_design.relative_position - 1)
-
- expect(subject).to be_error.and(have_attributes(message: :not_adjacent))
- end
- end
-
context 'moving a design with neighbours' do
let(:current_design) { designs.first }
let(:previous_design) { designs.second }
diff --git a/spec/services/discussions/resolve_service_spec.rb b/spec/services/discussions/resolve_service_spec.rb
index 42c4ef52741..2e30455eb0a 100644
--- a/spec/services/discussions/resolve_service_spec.rb
+++ b/spec/services/discussions/resolve_service_spec.rb
@@ -24,6 +24,13 @@ RSpec.describe Discussions::ResolveService do
expect(discussion).to be_resolved
end
+ it 'tracks thread resolve usage data' do
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to receive(:track_resolve_thread_action).with(user: user)
+
+ service.execute
+ end
+
it 'executes the notification service' do
expect_next_instance_of(MergeRequests::ResolvedDiscussionNotificationService) do |instance|
expect(instance).to receive(:execute).with(discussion.noteable)
@@ -101,6 +108,13 @@ RSpec.describe Discussions::ResolveService do
service.execute
end
+ it 'does not track thread resolve usage data' do
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .not_to receive(:track_resolve_thread_action).with(user: user)
+
+ service.execute
+ end
+
it 'does not schedule an auto-merge' do
expect(AutoMergeProcessWorker).not_to receive(:perform_async)
diff --git a/spec/services/discussions/unresolve_service_spec.rb b/spec/services/discussions/unresolve_service_spec.rb
new file mode 100644
index 00000000000..6298a00a474
--- /dev/null
+++ b/spec/services/discussions/unresolve_service_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Discussions::UnresolveService do
+ describe "#execute" do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user, developer_projects: [project]) }
+ let_it_be(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds, source_project: project) }
+ let(:discussion) { create(:diff_note_on_merge_request, noteable: merge_request, project: project).to_discussion }
+
+ let(:service) { described_class.new(discussion, user) }
+
+ before do
+ project.add_developer(user)
+ discussion.resolve!(user)
+ end
+
+ it "unresolves the discussion" do
+ service.execute
+
+ expect(discussion).not_to be_resolved
+ end
+
+ it "counts the unresolve event" do
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to receive(:track_unresolve_thread_action).with(user: user)
+
+ service.execute
+ end
+ end
+end
diff --git a/spec/services/feature_flags/create_service_spec.rb b/spec/services/feature_flags/create_service_spec.rb
index e115d8098c9..128fab114fe 100644
--- a/spec/services/feature_flags/create_service_spec.rb
+++ b/spec/services/feature_flags/create_service_spec.rb
@@ -66,18 +66,6 @@ RSpec.describe FeatureFlags::CreateService do
subject
end
- context 'the feature flag is disabled' do
- before do
- stub_feature_flags(jira_sync_feature_flags: false)
- end
-
- it 'does not sync the feature flag to Jira' do
- expect(::JiraConnect::SyncFeatureFlagsWorker).not_to receive(:perform_async)
-
- subject
- end
- end
-
it 'creates audit event' do
expected_message = 'Created feature flag <strong>feature_flag</strong> '\
'with description <strong>"description"</strong>. '\
diff --git a/spec/services/feature_flags/update_service_spec.rb b/spec/services/feature_flags/update_service_spec.rb
index 8c4055ddd9e..9639cf3081d 100644
--- a/spec/services/feature_flags/update_service_spec.rb
+++ b/spec/services/feature_flags/update_service_spec.rb
@@ -26,18 +26,6 @@ RSpec.describe FeatureFlags::UpdateService do
expect(subject[:status]).to eq(:success)
end
- context 'the feature flag is disabled' do
- before do
- stub_feature_flags(jira_sync_feature_flags: false)
- end
-
- it 'does not sync the feature flag to Jira' do
- expect(::JiraConnect::SyncFeatureFlagsWorker).not_to receive(:perform_async)
-
- subject
- end
- end
-
it 'syncs the feature flag to Jira' do
expect(::JiraConnect::SyncFeatureFlagsWorker).to receive(:perform_async).with(Integer, Integer)
diff --git a/spec/services/git/branch_hooks_service_spec.rb b/spec/services/git/branch_hooks_service_spec.rb
index a5290f0be68..52df21897b9 100644
--- a/spec/services/git/branch_hooks_service_spec.rb
+++ b/spec/services/git/branch_hooks_service_spec.rb
@@ -11,7 +11,8 @@ RSpec.describe Git::BranchHooksService do
let(:branch) { project.default_branch }
let(:ref) { "refs/heads/#{branch}" }
- let(:commit) { project.commit(sample_commit.id) }
+ let(:commit_id) { sample_commit.id }
+ let(:commit) { project.commit(commit_id) }
let(:oldrev) { commit.parent_id }
let(:newrev) { commit.id }
@@ -93,12 +94,12 @@ RSpec.describe Git::BranchHooksService do
describe 'Push Event' do
let(:event) { Event.pushed_action.first }
- before do
- service.execute
- end
+ subject(:execute_service) { service.execute }
context "with an existing branch" do
it 'generates a push event with one commit' do
+ execute_service
+
expect(event).to be_an_instance_of(PushEvent)
expect(event.project).to eq(project)
expect(event).to be_pushed_action
@@ -109,12 +110,87 @@ RSpec.describe Git::BranchHooksService do
expect(event.push_event_payload.ref).to eq('master')
expect(event.push_event_payload.commit_count).to eq(1)
end
+
+ context 'with changing CI config' do
+ before do
+ allow_next_instance_of(Gitlab::Git::Diff) do |diff|
+ allow(diff).to receive(:new_path).and_return('.gitlab-ci.yml')
+ end
+
+ allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event)
+ end
+
+ let!(:commit_author) { create(:user, email: sample_commit.author_email) }
+
+ let(:tracking_params) do
+ ['o_pipeline_authoring_unique_users_committing_ciconfigfile', values: commit_author.id]
+ end
+
+ it 'tracks the event' do
+ execute_service
+
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter)
+ .to have_received(:track_event).with(*tracking_params)
+ end
+
+ context 'when the FF usage_data_unique_users_committing_ciconfigfile is disabled' do
+ before do
+ stub_feature_flags(usage_data_unique_users_committing_ciconfigfile: false)
+ end
+
+ it 'does not track the event' do
+ execute_service
+
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter)
+ .not_to have_received(:track_event).with(*tracking_params)
+ end
+ end
+
+ context 'when usage ping is disabled' do
+ before do
+ stub_application_setting(usage_ping_enabled: false)
+ end
+
+ it 'does not track the event' do
+ execute_service
+
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter)
+ .not_to have_received(:track_event).with(*tracking_params)
+ end
+ end
+
+ context 'when the branch is not the main branch' do
+ let(:branch) { 'feature' }
+
+ it 'does not track the event' do
+ execute_service
+
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter)
+ .not_to have_received(:track_event).with(*tracking_params)
+ end
+ end
+
+ context 'when the CI config is a different path' do
+ before do
+ project.ci_config_path = 'config/ci.yml'
+ end
+
+ it 'does not track the event' do
+ execute_service
+
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter)
+ .not_to have_received(:track_event).with(*tracking_params)
+ end
+ end
+ end
end
context "with a new branch" do
let(:oldrev) { Gitlab::Git::BLANK_SHA }
it 'generates a push event with more than one commit' do
+ execute_service
+
expect(event).to be_an_instance_of(PushEvent)
expect(event.project).to eq(project)
expect(event).to be_pushed_action
@@ -131,6 +207,8 @@ RSpec.describe Git::BranchHooksService do
let(:newrev) { Gitlab::Git::BLANK_SHA }
it 'generates a push event with no commits' do
+ execute_service
+
expect(event).to be_an_instance_of(PushEvent)
expect(event.project).to eq(project)
expect(event).to be_pushed_action
@@ -150,7 +228,6 @@ RSpec.describe Git::BranchHooksService do
)
end
- let(:commit) { project.repository.commit(commit_id) }
let(:blank_sha) { Gitlab::Git::BLANK_SHA }
def clears_cache(extended: [])
@@ -431,11 +508,7 @@ RSpec.describe Git::BranchHooksService do
end
describe 'Metrics dashboard sync' do
- context 'with feature flag enabled' do
- before do
- Feature.enable(:metrics_dashboards_sync)
- end
-
+ shared_examples 'trigger dashboard sync' do
it 'imports metrics to database' do
expect(Metrics::Dashboard::SyncDashboardsWorker).to receive(:perform_async)
@@ -443,12 +516,95 @@ RSpec.describe Git::BranchHooksService do
end
end
- context 'with feature flag disabled' do
- it 'imports metrics to database' do
- expect(Metrics::Dashboard::SyncDashboardsWorker).to receive(:perform_async)
+ shared_examples 'no dashboard sync' do
+ it 'does not sync metrics to database' do
+ expect(Metrics::Dashboard::SyncDashboardsWorker).not_to receive(:perform_async)
service.execute
end
end
+
+ def change_repository(**changes)
+ actions = changes.flat_map do |(action, paths)|
+ Array(paths).flat_map do |file_path|
+ { action: action, file_path: file_path, content: SecureRandom.hex }
+ end
+ end
+
+ project.repository.multi_action(
+ user, message: 'message', branch_name: branch, actions: actions
+ )
+ end
+
+ let(:charts) { '.gitlab/dashboards/charts.yml' }
+ let(:readme) { 'README.md' }
+ let(:commit_id) { change_repository(**commit_changes) }
+
+ context 'with default branch' do
+ context 'when adding files' do
+ let(:new_file) { 'somenewfile.md' }
+
+ context 'also related' do
+ let(:commit_changes) { { create: [charts, new_file] } }
+
+ include_examples 'trigger dashboard sync'
+ end
+
+ context 'only unrelated' do
+ let(:commit_changes) { { create: new_file } }
+
+ include_examples 'no dashboard sync'
+ end
+ end
+
+ context 'when deleting files' do
+ before do
+ change_repository(create: charts)
+ end
+
+ context 'also related' do
+ let(:commit_changes) { { delete: [charts, readme] } }
+
+ include_examples 'trigger dashboard sync'
+ end
+
+ context 'only unrelated' do
+ let(:commit_changes) { { delete: readme } }
+
+ include_examples 'no dashboard sync'
+ end
+ end
+
+ context 'when updating files' do
+ before do
+ change_repository(create: charts)
+ end
+
+ context 'also related' do
+ let(:commit_changes) { { update: [charts, readme] } }
+
+ include_examples 'trigger dashboard sync'
+ end
+
+ context 'only unrelated' do
+ let(:commit_changes) { { update: readme } }
+
+ include_examples 'no dashboard sync'
+ end
+ end
+
+ context 'without changes' do
+ let(:commit_changes) { {} }
+
+ include_examples 'no dashboard sync'
+ end
+ end
+
+ context 'with other branch' do
+ let(:branch) { 'fix' }
+ let(:commit_changes) { { create: charts } }
+
+ include_examples 'no dashboard sync'
+ end
end
end
diff --git a/spec/services/git/wiki_push_service_spec.rb b/spec/services/git/wiki_push_service_spec.rb
index cd38f2e97fb..df9a48d7b1c 100644
--- a/spec/services/git/wiki_push_service_spec.rb
+++ b/spec/services/git/wiki_push_service_spec.rb
@@ -257,6 +257,46 @@ RSpec.describe Git::WikiPushService, services: true do
end
end
+ describe '#perform_housekeeping', :clean_gitlab_redis_shared_state do
+ let(:housekeeping) { Repositories::HousekeepingService.new(wiki) }
+
+ subject { create_service(current_sha).execute }
+
+ before do
+ allow(Repositories::HousekeepingService).to receive(:new).and_return(housekeeping)
+ end
+
+ it 'does not perform housekeeping when not needed' do
+ expect(housekeeping).not_to receive(:execute)
+
+ subject
+ end
+
+ context 'when housekeeping is needed' do
+ before do
+ allow(housekeeping).to receive(:needed?).and_return(true)
+ end
+
+ it 'performs housekeeping' do
+ expect(housekeeping).to receive(:execute)
+
+ subject
+ end
+
+ it 'does not raise an exception' do
+ allow(housekeeping).to receive(:try_obtain_lease).and_return(false)
+
+ expect { subject }.not_to raise_error
+ end
+ end
+
+ it 'increments the push counter' do
+ expect(housekeeping).to receive(:increment!)
+
+ subject
+ end
+ end
+
# In order to construct the correct GitPostReceive object that represents the
# changes we are applying, we need to describe the changes between old-ref and
# new-ref. Old ref (the base sha) we have to capture before we perform any
diff --git a/spec/services/groups/import_export/export_service_spec.rb b/spec/services/groups/import_export/export_service_spec.rb
index 690bcb94556..d6ce40f413b 100644
--- a/spec/services/groups/import_export/export_service_spec.rb
+++ b/spec/services/groups/import_export/export_service_spec.rb
@@ -71,6 +71,16 @@ RSpec.describe Groups::ImportExport::ExportService do
service.execute
end
+ it 'compresses and removes tmp files' do
+ expect(group.import_export_upload).to be_nil
+ expect(Gitlab::ImportExport::Saver).to receive(:new).and_call_original
+
+ service.execute
+
+ expect(Dir.exist?(shared.archive_path)).to eq false
+ expect(File.exist?(group.import_export_upload.export_file.path)).to eq true
+ end
+
it 'notifies the user' do
expect_next_instance_of(NotificationService) do |instance|
expect(instance).to receive(:group_was_exported)
@@ -134,7 +144,7 @@ RSpec.describe Groups::ImportExport::ExportService do
expect { service.execute }.to raise_error(Gitlab::ImportExport::Error)
expect(group.import_export_upload).to be_nil
- expect(Dir.exist?(shared.base_path)).to eq(false)
+ expect(Dir.exist?(shared.archive_path)).to eq(false)
end
it 'notifies the user about failed group export' do
@@ -159,7 +169,7 @@ RSpec.describe Groups::ImportExport::ExportService do
expect { service.execute }.to raise_error(Gitlab::ImportExport::Error)
expect(group.import_export_upload).to be_nil
- expect(Dir.exist?(shared.base_path)).to eq(false)
+ expect(Dir.exist?(shared.archive_path)).to eq(false)
end
it 'notifies logger' do
diff --git a/spec/services/groups/open_issues_count_service_spec.rb b/spec/services/groups/open_issues_count_service_spec.rb
new file mode 100644
index 00000000000..8bbb1c90c6b
--- /dev/null
+++ b/spec/services/groups/open_issues_count_service_spec.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Groups::OpenIssuesCountService, :use_clean_rails_memory_store_caching do
+ let_it_be(:group) { create(:group, :public)}
+ let_it_be(:project) { create(:project, :public, namespace: group) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:issue) { create(:issue, :opened, project: project) }
+ let_it_be(:confidential) { create(:issue, :opened, confidential: true, project: project) }
+ let_it_be(:closed) { create(:issue, :closed, project: project) }
+
+ subject { described_class.new(group, user) }
+
+ describe '#relation_for_count' do
+ before do
+ allow(IssuesFinder).to receive(:new).and_call_original
+ end
+
+ it 'uses the IssuesFinder to scope issues' do
+ expect(IssuesFinder)
+ .to receive(:new)
+ .with(user, group_id: group.id, state: 'opened', non_archived: true, include_subgroups: true, public_only: true)
+
+ subject.count
+ end
+ end
+
+ describe '#count' do
+ context 'when user is nil' do
+ it 'does not include confidential issues in the issue count' do
+ expect(described_class.new(group).count).to eq(1)
+ end
+ end
+
+ context 'when user is provided' do
+ context 'when user can read confidential issues' do
+ before do
+ group.add_reporter(user)
+ end
+
+ it 'returns the right count with confidential issues' do
+ expect(subject.count).to eq(2)
+ end
+ end
+
+ context 'when user cannot read confidential issues' do
+ before do
+ group.add_guest(user)
+ end
+
+ it 'does not include confidential issues' do
+ expect(subject.count).to eq(1)
+ end
+ end
+
+ context 'with different cache values' do
+ let(:public_count_key) { subject.cache_key(described_class::PUBLIC_COUNT_KEY) }
+ let(:under_threshold) { described_class::CACHED_COUNT_THRESHOLD - 1 }
+ let(:over_threshold) { described_class::CACHED_COUNT_THRESHOLD + 1 }
+
+ context 'when cache is empty' do
+ before do
+ Rails.cache.delete(public_count_key)
+ end
+
+ it 'refreshes cache if value over threshold' do
+ allow(subject).to receive(:uncached_count).and_return(over_threshold)
+
+ expect(subject.count).to eq(over_threshold)
+ expect(Rails.cache.read(public_count_key)).to eq(over_threshold)
+ end
+
+ it 'does not refresh cache if value under threshold' do
+ allow(subject).to receive(:uncached_count).and_return(under_threshold)
+
+ expect(subject.count).to eq(under_threshold)
+ expect(Rails.cache.read(public_count_key)).to be_nil
+ end
+ end
+
+ context 'when cached count is under the threshold value' do
+ before do
+ Rails.cache.write(public_count_key, under_threshold)
+ end
+
+ it 'does not refresh cache' do
+ expect(Rails.cache).not_to receive(:write)
+ expect(subject.count).to eq(under_threshold)
+ end
+ end
+
+ context 'when cached count is over the threshold value' do
+ before do
+ Rails.cache.write(public_count_key, over_threshold)
+ end
+
+ it 'does not refresh cache' do
+ expect(Rails.cache).not_to receive(:write)
+ expect(subject.count).to eq(over_threshold)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/integrations/test/project_service_spec.rb b/spec/services/integrations/test/project_service_spec.rb
index dd603765d59..052b25b0f10 100644
--- a/spec/services/integrations/test/project_service_spec.rb
+++ b/spec/services/integrations/test/project_service_spec.rb
@@ -3,11 +3,12 @@
require 'spec_helper'
RSpec.describe Integrations::Test::ProjectService do
- let(:user) { double('user') }
+ include AfterNextHelpers
describe '#execute' do
- let(:project) { create(:project) }
+ let_it_be(:project) { create(:project) }
let(:integration) { create(:slack_service, project: project) }
+ let(:user) { project.owner }
let(:event) { nil }
let(:sample_data) { { data: 'sample' } }
let(:success_result) { { success: true, result: {} } }
@@ -70,16 +71,17 @@ RSpec.describe Integrations::Test::ProjectService do
end
it 'executes integration' do
- allow(project).to receive(:notes).and_return([Note.new])
+ create(:note, project: project)
+
allow(Gitlab::DataBuilder::Note).to receive(:build).and_return(sample_data)
+ allow_next(NotesFinder).to receive(:execute).and_return(Note.all)
expect(integration).to receive(:test).with(sample_data).and_return(success_result)
expect(subject).to eq(success_result)
end
end
- context 'issue' do
- let(:event) { 'issue' }
+ shared_examples_for 'a test of an integration that operates on issues' do
let(:issue) { build(:issue) }
it 'returns error message if not enough data' do
@@ -90,32 +92,28 @@ RSpec.describe Integrations::Test::ProjectService do
it 'executes integration' do
allow(project).to receive(:issues).and_return([issue])
allow(issue).to receive(:to_hook_data).and_return(sample_data)
+ allow_next(IssuesFinder).to receive(:execute).and_return([issue])
expect(integration).to receive(:test).with(sample_data).and_return(success_result)
expect(subject).to eq(success_result)
end
end
- context 'confidential_issue' do
- let(:event) { 'confidential_issue' }
- let(:issue) { build(:issue) }
+ context 'issue' do
+ let(:event) { 'issue' }
- it 'returns error message if not enough data' do
- expect(integration).not_to receive(:test)
- expect(subject).to include({ status: :error, message: 'Ensure the project has issues.' })
- end
+ it_behaves_like 'a test of an integration that operates on issues'
+ end
- it 'executes integration' do
- allow(project).to receive(:issues).and_return([issue])
- allow(issue).to receive(:to_hook_data).and_return(sample_data)
+ context 'confidential_issue' do
+ let(:event) { 'confidential_issue' }
- expect(integration).to receive(:test).with(sample_data).and_return(success_result)
- expect(subject).to eq(success_result)
- end
+ it_behaves_like 'a test of an integration that operates on issues'
end
context 'merge_request' do
let(:event) { 'merge_request' }
+ let(:merge_request) { build(:merge_request) }
it 'returns error message if not enough data' do
expect(integration).not_to receive(:test)
@@ -123,16 +121,17 @@ RSpec.describe Integrations::Test::ProjectService do
end
it 'executes integration' do
- create(:merge_request, source_project: project)
- allow_any_instance_of(MergeRequest).to receive(:to_hook_data).and_return(sample_data)
+ allow(merge_request).to receive(:to_hook_data).and_return(sample_data)
+ allow_next(MergeRequestsFinder).to receive(:execute).and_return([merge_request])
expect(integration).to receive(:test).with(sample_data).and_return(success_result)
- expect(subject).to eq(success_result)
+ expect(subject).to include(success_result)
end
end
context 'deployment' do
- let(:project) { create(:project, :test_repo) }
+ let_it_be(:project) { create(:project, :test_repo) }
+ let(:deployment) { build(:deployment) }
let(:event) { 'deployment' }
it 'returns error message if not enough data' do
@@ -141,8 +140,8 @@ RSpec.describe Integrations::Test::ProjectService do
end
it 'executes integration' do
- create(:deployment, project: project)
allow(Gitlab::DataBuilder::Deployment).to receive(:build).and_return(sample_data)
+ allow_next(DeploymentsFinder).to receive(:execute).and_return([deployment])
expect(integration).to receive(:test).with(sample_data).and_return(success_result)
expect(subject).to eq(success_result)
@@ -151,6 +150,7 @@ RSpec.describe Integrations::Test::ProjectService do
context 'pipeline' do
let(:event) { 'pipeline' }
+ let(:pipeline) { build(:ci_pipeline) }
it 'returns error message if not enough data' do
expect(integration).not_to receive(:test)
@@ -158,8 +158,8 @@ RSpec.describe Integrations::Test::ProjectService do
end
it 'executes integration' do
- create(:ci_empty_pipeline, project: project)
allow(Gitlab::DataBuilder::Pipeline).to receive(:build).and_return(sample_data)
+ allow_next(Ci::PipelinesFinder).to receive(:execute).and_return([pipeline])
expect(integration).to receive(:test).with(sample_data).and_return(success_result)
expect(subject).to eq(success_result)
@@ -167,7 +167,7 @@ RSpec.describe Integrations::Test::ProjectService do
end
context 'wiki_page' do
- let(:project) { create(:project, :wiki_repo) }
+ let_it_be(:project) { create(:project, :wiki_repo) }
let(:event) { 'wiki_page' }
it 'returns error message if wiki disabled' do
diff --git a/spec/services/issue_rebalancing_service_spec.rb b/spec/services/issue_rebalancing_service_spec.rb
index 94f594c8083..7b3d4213b24 100644
--- a/spec/services/issue_rebalancing_service_spec.rb
+++ b/spec/services/issue_rebalancing_service_spec.rb
@@ -32,70 +32,88 @@ RSpec.describe IssueRebalancingService do
project.reload.issues.reorder(relative_position: :asc).to_a
end
- it 'rebalances a set of issues with clumps at the end and start' do
- all_issues = start_clump + unclumped + end_clump.reverse
- service = described_class.new(project.issues.first)
+ shared_examples 'IssueRebalancingService shared examples' do
+ it 'rebalances a set of issues with clumps at the end and start' do
+ all_issues = start_clump + unclumped + end_clump.reverse
+ service = described_class.new(project.issues.first)
- expect { service.execute }.not_to change { issues_in_position_order.map(&:id) }
+ expect { service.execute }.not_to change { issues_in_position_order.map(&:id) }
- all_issues.each(&:reset)
+ all_issues.each(&:reset)
- gaps = all_issues.take(all_issues.count - 1).zip(all_issues.drop(1)).map do |a, b|
- b.relative_position - a.relative_position
+ gaps = all_issues.take(all_issues.count - 1).zip(all_issues.drop(1)).map do |a, b|
+ b.relative_position - a.relative_position
+ end
+
+ expect(gaps).to all(be > RelativePositioning::MIN_GAP)
+ expect(all_issues.first.relative_position).to be > (RelativePositioning::MIN_POSITION * 0.9999)
+ expect(all_issues.last.relative_position).to be < (RelativePositioning::MAX_POSITION * 0.9999)
end
- expect(gaps).to all(be > RelativePositioning::MIN_GAP)
- expect(all_issues.first.relative_position).to be > (RelativePositioning::MIN_POSITION * 0.9999)
- expect(all_issues.last.relative_position).to be < (RelativePositioning::MAX_POSITION * 0.9999)
- end
+ it 'is idempotent' do
+ service = described_class.new(project.issues.first)
- it 'is idempotent' do
- service = described_class.new(project.issues.first)
+ expect do
+ service.execute
+ service.execute
+ end.not_to change { issues_in_position_order.map(&:id) }
+ end
- expect do
- service.execute
- service.execute
- end.not_to change { issues_in_position_order.map(&:id) }
- end
+ it 'does nothing if the feature flag is disabled' do
+ stub_feature_flags(rebalance_issues: false)
+ issue = project.issues.first
+ issue.project
+ issue.project.group
+ old_pos = issue.relative_position
- it 'does nothing if the feature flag is disabled' do
- stub_feature_flags(rebalance_issues: false)
- issue = project.issues.first
- issue.project
- issue.project.group
- old_pos = issue.relative_position
+ service = described_class.new(issue)
- service = described_class.new(issue)
+ expect { service.execute }.not_to exceed_query_limit(0)
+ expect(old_pos).to eq(issue.reload.relative_position)
+ end
- expect { service.execute }.not_to exceed_query_limit(0)
- expect(old_pos).to eq(issue.reload.relative_position)
- end
+ it 'acts if the flag is enabled for the project' do
+ issue = create(:issue, project: project, author: user, relative_position: max_pos)
+ stub_feature_flags(rebalance_issues: issue.project)
- it 'acts if the flag is enabled for the project' do
- issue = create(:issue, project: project, author: user, relative_position: max_pos)
- stub_feature_flags(rebalance_issues: issue.project)
+ service = described_class.new(issue)
- service = described_class.new(issue)
+ expect { service.execute }.to change { issue.reload.relative_position }
+ end
- expect { service.execute }.to change { issue.reload.relative_position }
- end
+ it 'acts if the flag is enabled for the group' do
+ issue = create(:issue, project: project, author: user, relative_position: max_pos)
+ project.update!(group: create(:group))
+ stub_feature_flags(rebalance_issues: issue.project.group)
- it 'acts if the flag is enabled for the group' do
- issue = create(:issue, project: project, author: user, relative_position: max_pos)
- project.update!(group: create(:group))
- stub_feature_flags(rebalance_issues: issue.project.group)
+ service = described_class.new(issue)
- service = described_class.new(issue)
+ expect { service.execute }.to change { issue.reload.relative_position }
+ end
+
+ it 'aborts if there are too many issues' do
+ issue = project.issues.first
+ base = double(count: 10_001)
- expect { service.execute }.to change { issue.reload.relative_position }
+ allow(Issue).to receive(:relative_positioning_query_base).with(issue).and_return(base)
+
+ expect { described_class.new(issue).execute }.to raise_error(described_class::TooManyIssues)
+ end
end
- it 'aborts if there are too many issues' do
- issue = project.issues.first
- base = double(count: 10_001)
+ context 'when issue_rebalancing_optimization feature flag is on' do
+ before do
+ stub_feature_flags(issue_rebalancing_optimization: true)
+ end
+
+ it_behaves_like 'IssueRebalancingService shared examples'
+ end
- allow(Issue).to receive(:relative_positioning_query_base).with(issue).and_return(base)
+ context 'when issue_rebalancing_optimization feature flag is on' do
+ before do
+ stub_feature_flags(issue_rebalancing_optimization: false)
+ end
- expect { described_class.new(issue).execute }.to raise_error(described_class::TooManyIssues)
+ it_behaves_like 'IssueRebalancingService shared examples'
end
end
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index dc545f57d23..3cf45143594 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -84,6 +84,7 @@ RSpec.describe Issues::CloseService do
let!(:external_issue_tracker) { create(:jira_service, project: project) }
it 'closes the issue on the external issue tracker' do
+ project.reload
expect(project.external_issue_tracker).to receive(:close_issue)
described_class.new(project, user).close_issue(external_issue)
@@ -94,6 +95,7 @@ RSpec.describe Issues::CloseService do
let!(:external_issue_tracker) { create(:jira_service, project: project, active: false) }
it 'does not close the issue on the external issue tracker' do
+ project.reload
expect(project.external_issue_tracker).not_to receive(:close_issue)
described_class.new(project, user).close_issue(external_issue)
@@ -104,6 +106,7 @@ RSpec.describe Issues::CloseService do
let!(:external_issue_tracker) { create(:bugzilla_service, project: project) }
it 'does not close the issue on the external issue tracker' do
+ project.reload
expect(project.external_issue_tracker).not_to receive(:close_issue)
described_class.new(project, user).close_issue(external_issue)
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index cc6a49fc4cf..e42e9722297 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -452,162 +452,50 @@ RSpec.describe Issues::CreateService do
end
context 'checking spam' do
- include_context 'includes Spam constants'
+ let(:request) { double(:request) }
+ let(:api) { true }
+ let(:captcha_response) { 'abc123' }
+ let(:spam_log_id) { 1 }
- let(:title) { 'Legit issue' }
- let(:description) { 'please fix' }
- let(:opts) do
+ let(:params) do
{
- title: title,
- description: description,
- request: double(:request, env: {})
+ title: 'Spam issue',
+ request: request,
+ api: api,
+ captcha_response: captcha_response,
+ spam_log_id: spam_log_id
}
end
- subject { described_class.new(project, user, opts) }
-
- before do
- stub_feature_flags(allow_possible_spam: false)
+ subject do
+ described_class.new(project, user, params)
end
- context 'when reCAPTCHA was verified' do
- let(:log_user) { user }
- let(:spam_logs) { create_list(:spam_log, 2, user: log_user, title: title) }
- let(:target_spam_log) { spam_logs.last }
-
- before do
- opts[:recaptcha_verified] = true
- opts[:spam_log_id] = target_spam_log.id
-
- expect(Spam::SpamVerdictService).not_to receive(:new)
- end
-
- it 'does not mark an issue as spam' do
- expect(issue).not_to be_spam
- end
-
- it 'creates a valid issue' do
- expect(issue).to be_valid
- end
-
- it 'does not assign a spam_log to the issue' do
- expect(issue.spam_log).to be_nil
- end
-
- it 'marks related spam_log as recaptcha_verified' do
- expect { issue }.to change { target_spam_log.reload.recaptcha_verified }.from(false).to(true)
- end
-
- context 'when spam log does not belong to a user' do
- let(:log_user) { create(:user) }
-
- it 'does not mark spam_log as recaptcha_verified' do
- expect { issue }.not_to change { target_spam_log.reload.recaptcha_verified }
- end
+ before do
+ allow_next_instance_of(UserAgentDetailService) do |instance|
+ allow(instance).to receive(:create)
end
end
- context 'when reCAPTCHA was not verified' do
- before do
- expect_next_instance_of(Spam::SpamActionService) do |spam_service|
- expect(spam_service).to receive_messages(check_for_spam?: true)
- end
- end
-
- context 'when SpamVerdictService requires reCAPTCHA' do
- before do
- expect_next_instance_of(Spam::SpamVerdictService) do |verdict_service|
- expect(verdict_service).to receive(:execute).and_return(CONDITIONAL_ALLOW)
- end
- end
-
- it 'does not mark the issue as spam' do
- expect(issue).not_to be_spam
- end
-
- it 'marks the issue as needing reCAPTCHA' do
- expect(issue.needs_recaptcha?).to be_truthy
- end
-
- it 'invalidates the issue' do
- expect(issue).to be_invalid
- end
-
- it 'creates a new spam_log' do
- expect { issue }
- .to have_spam_log(title: title, description: description, user_id: user.id, noteable_type: 'Issue')
- end
- end
-
- context 'when SpamVerdictService disallows creation' do
- before do
- expect_next_instance_of(Spam::SpamVerdictService) do |verdict_service|
- expect(verdict_service).to receive(:execute).and_return(DISALLOW)
- end
- end
-
- context 'when allow_possible_spam feature flag is false' do
- it 'marks the issue as spam' do
- expect(issue).to be_spam
- end
-
- it 'does not mark the issue as needing reCAPTCHA' do
- expect(issue.needs_recaptcha?).to be_falsey
- end
-
- it 'invalidates the issue' do
- expect(issue).to be_invalid
- end
-
- it 'creates a new spam_log' do
- expect { issue }
- .to have_spam_log(title: title, description: description, user_id: user.id, noteable_type: 'Issue')
- end
- end
-
- context 'when allow_possible_spam feature flag is true' do
- before do
- stub_feature_flags(allow_possible_spam: true)
- end
-
- it 'does not mark the issue as spam' do
- expect(issue).not_to be_spam
- end
-
- it 'does not mark the issue as needing reCAPTCHA' do
- expect(issue.needs_recaptcha?).to be_falsey
- end
-
- it 'creates a valid issue' do
- expect(issue).to be_valid
- end
-
- it 'creates a new spam_log' do
- expect { issue }
- .to have_spam_log(title: title, description: description, user_id: user.id, noteable_type: 'Issue')
- end
- end
+ it 'executes SpamActionService' do
+ spam_params = Spam::SpamParams.new(
+ api: api,
+ captcha_response: captcha_response,
+ spam_log_id: spam_log_id
+ )
+ expect_next_instance_of(
+ Spam::SpamActionService,
+ {
+ spammable: an_instance_of(Issue),
+ request: request,
+ user: user,
+ action: :create
+ }
+ ) do |instance|
+ expect(instance).to receive(:execute).with(spam_params: spam_params)
end
- context 'when the SpamVerdictService allows creation' do
- before do
- expect_next_instance_of(Spam::SpamVerdictService) do |verdict_service|
- expect(verdict_service).to receive(:execute).and_return(ALLOW)
- end
- end
-
- it 'does not mark an issue as spam' do
- expect(issue).not_to be_spam
- end
-
- it 'creates a valid issue' do
- expect(issue).to be_valid
- end
-
- it 'does not assign a spam_log to an issue' do
- expect(issue.spam_log).to be_nil
- end
- end
+ subject.execute
end
end
end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 06a6a52bc41..fd42a84e405 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -711,7 +711,7 @@ RSpec.describe Issues::UpdateService, :mailer do
}
service = described_class.new(project, user, params)
- expect(service).not_to receive(:spam_check)
+ expect(Spam::SpamActionService).not_to receive(:new)
service.execute(issue)
end
diff --git a/spec/services/members/update_service_spec.rb b/spec/services/members/update_service_spec.rb
index 2d4457f3f62..a1b1397d444 100644
--- a/spec/services/members/update_service_spec.rb
+++ b/spec/services/members/update_service_spec.rb
@@ -13,9 +13,11 @@ RSpec.describe Members::UpdateService do
{ access_level: Gitlab::Access::MAINTAINER }
end
+ subject { described_class.new(current_user, params).execute(member, permission: permission) }
+
shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do
it 'raises Gitlab::Access::AccessDeniedError' do
- expect { described_class.new(current_user, params).execute(member, permission: permission) }
+ expect { subject }
.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
@@ -24,18 +26,24 @@ RSpec.describe Members::UpdateService 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)
- updated_member = described_class.new(current_user, params).execute(member, permission: permission)
+ updated_member = subject.fetch(:member)
expect(updated_member).to be_valid
expect(updated_member.access_level).to eq(Gitlab::Access::MAINTAINER)
end
+ it 'returns success status' do
+ result = subject.fetch(:status)
+
+ expect(result).to eq(:success)
+ 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
- updated_member = described_class.new(current_user, params).execute(member, permission: permission)
+ updated_member = subject.fetch(:member)
expect(updated_member).to be_valid
expect(updated_member.access_level).to eq(Gitlab::Access::GUEST)
@@ -62,6 +70,16 @@ RSpec.describe Members::UpdateService do
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 } }
+
+ it 'returns error status' do
+ result = subject
+
+ expect(result[:status]).to eq(:error)
+ end
+ end
end
before do
diff --git a/spec/services/merge_requests/after_create_service_spec.rb b/spec/services/merge_requests/after_create_service_spec.rb
index 9ae310d8cee..f21feb70bc5 100644
--- a/spec/services/merge_requests/after_create_service_spec.rb
+++ b/spec/services/merge_requests/after_create_service_spec.rb
@@ -3,8 +3,6 @@
require 'spec_helper'
RSpec.describe MergeRequests::AfterCreateService do
- include AfterNextHelpers
-
let_it_be(:merge_request) { create(:merge_request) }
subject(:after_create_service) do
@@ -66,15 +64,8 @@ RSpec.describe MergeRequests::AfterCreateService do
execute_service
end
- it 'registers an onboarding progress action' do
- OnboardingProgress.onboard(merge_request.target_project.namespace)
-
- expect_next(OnboardingProgressService, merge_request.target_project.namespace)
- .to receive(:execute).with(action: :merge_request_created).and_call_original
-
- execute_service
-
- expect(OnboardingProgress.completed?(merge_request.target_project.namespace, :merge_request_created)).to be(true)
+ it_behaves_like 'records an onboarding progress action', :merge_request_created do
+ let(:namespace) { merge_request.target_project.namespace }
end
end
end
diff --git a/spec/services/merge_requests/approval_service_spec.rb b/spec/services/merge_requests/approval_service_spec.rb
index 124501f17d5..df9a98c5540 100644
--- a/spec/services/merge_requests/approval_service_spec.rb
+++ b/spec/services/merge_requests/approval_service_spec.rb
@@ -31,6 +31,13 @@ RSpec.describe MergeRequests::ApprovalService do
expect(todo.reload).to be_pending
end
+
+ it 'does not track merge request approve action' do
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .not_to receive(:track_approve_mr_action).with(user: user)
+
+ service.execute(merge_request)
+ end
end
context 'with valid approval' do
@@ -59,6 +66,13 @@ RSpec.describe MergeRequests::ApprovalService do
service.execute(merge_request)
end
end
+
+ it 'tracks merge request approve action' do
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to receive(:track_approve_mr_action).with(user: user)
+
+ service.execute(merge_request)
+ end
end
context 'user cannot update the merge request' do
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index f83b8d98ce8..22b3456708f 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -252,6 +252,7 @@ RSpec.describe MergeRequests::BuildService do
issue.update!(iid: 123)
else
create(:"#{issue_tracker}_service", project: project)
+ project.reload
end
end
@@ -351,6 +352,7 @@ RSpec.describe MergeRequests::BuildService do
issue.update!(iid: 123)
else
create(:"#{issue_tracker}_service", project: project)
+ project.reload
end
end
diff --git a/spec/services/merge_requests/create_from_issue_service_spec.rb b/spec/services/merge_requests/create_from_issue_service_spec.rb
index be8c41bc4a1..6528edfc8b7 100644
--- a/spec/services/merge_requests/create_from_issue_service_spec.rb
+++ b/spec/services/merge_requests/create_from_issue_service_spec.rb
@@ -52,6 +52,14 @@ RSpec.describe MergeRequests::CreateFromIssueService do
service.execute
end
+ it 'tracks the mr creation when the mr is valid' do
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to receive(:track_mr_create_from_issue)
+ .with(user: user)
+
+ service.execute
+ end
+
it 'creates the new_issue_branch system note when the branch could be created but the merge_request cannot be created', :sidekiq_might_not_need_inline do
expect_next_instance_of(MergeRequest) do |instance|
expect(instance).to receive(:valid?).at_least(:once).and_return(false)
@@ -62,6 +70,17 @@ RSpec.describe MergeRequests::CreateFromIssueService do
service.execute
end
+ it 'does not track the mr creation when the Mr is invalid' do
+ expect_next_instance_of(MergeRequest) do |instance|
+ expect(instance).to receive(:valid?).at_least(:once).and_return(false)
+ end
+
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .not_to receive(:track_mr_create_from_issue)
+
+ service.execute
+ end
+
it 'creates a merge request', :sidekiq_might_not_need_inline do
expect { service.execute }.to change(target_project.merge_requests, :count).by(1)
end
diff --git a/spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb b/spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb
index cdaacaf5fca..d2070a466b1 100644
--- a/spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb
+++ b/spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb
@@ -12,6 +12,8 @@ RSpec.describe MergeRequests::DeleteNonLatestDiffsService, :clean_gitlab_redis_s
stub_const("#{described_class.name}::BATCH_SIZE", 2)
3.times { merge_request.create_merge_request_diff }
+ merge_request.create_merge_head_diff
+ merge_request.reset
end
it 'schedules non-latest merge request diffs removal' do
diff --git a/spec/services/merge_requests/mark_reviewer_reviewed_service_spec.rb b/spec/services/merge_requests/mark_reviewer_reviewed_service_spec.rb
new file mode 100644
index 00000000000..1075f6f9034
--- /dev/null
+++ b/spec/services/merge_requests/mark_reviewer_reviewed_service_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe MergeRequests::MarkReviewerReviewedService do
+ let(:current_user) { create(:user) }
+ let(:merge_request) { create(:merge_request, reviewers: [current_user]) }
+ let(:reviewer) { merge_request.merge_request_reviewers.find_by(user_id: current_user.id) }
+ let(:project) { merge_request.project }
+ let(:service) { described_class.new(project, current_user) }
+ let(:result) { service.execute(merge_request) }
+
+ before do
+ project.add_developer(current_user)
+ end
+
+ describe '#execute' do
+ describe 'invalid permissions' do
+ let(:service) { described_class.new(project, create(:user)) }
+
+ it 'returns an error' do
+ expect(result[:status]).to eq :error
+ end
+ end
+
+ describe 'reviewer does not exist' do
+ let(:service) { described_class.new(project, create(:user)) }
+
+ it 'returns an error' do
+ expect(result[:status]).to eq :error
+ end
+ end
+
+ describe 'reviewer exists' do
+ it 'returns success' do
+ expect(result[:status]).to eq :success
+ end
+
+ it 'updates reviewers state' do
+ expect(result[:status]).to eq :success
+ expect(reviewer.state).to eq 'reviewed'
+ end
+ end
+ end
+end
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index dd37d87e3f5..611f12c8146 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -161,7 +161,7 @@ RSpec.describe MergeRequests::MergeService do
commit = double('commit', safe_message: "Fixes #{jira_issue.to_reference}")
allow(merge_request).to receive(:commits).and_return([commit])
- expect_any_instance_of(JiraService).to receive(:close_issue).with(merge_request, jira_issue).once
+ expect_any_instance_of(JiraService).to receive(:close_issue).with(merge_request, jira_issue, user).once
service.execute(merge_request)
end
@@ -310,12 +310,12 @@ RSpec.describe MergeRequests::MergeService do
it 'logs and saves error if there is an exception' do
error_message = 'error message'
- allow(service).to receive(:repository).and_raise('error message')
+ allow(service).to receive(:repository).and_raise(error_message)
allow(service).to receive(:execute_hooks)
service.execute(merge_request)
- expect(merge_request.merge_error).to include('Something went wrong during merge')
+ expect(merge_request.merge_error).to eq(described_class::GENERIC_ERROR_MESSAGE)
expect(Gitlab::AppLogger).to have_received(:error).with(a_string_matching(error_message))
end
@@ -343,9 +343,7 @@ RSpec.describe MergeRequests::MergeService do
expect(Gitlab::AppLogger).to have_received(:error).with(a_string_matching(error_message))
end
- it 'logs and saves error if there is a merge conflict' do
- error_message = 'Conflicts detected during merge'
-
+ it 'logs and saves error if commit is not created' do
allow_any_instance_of(Repository).to receive(:merge).and_return(false)
allow(service).to receive(:execute_hooks)
@@ -353,8 +351,8 @@ RSpec.describe MergeRequests::MergeService do
expect(merge_request).to be_open
expect(merge_request.merge_commit_sha).to be_nil
- expect(merge_request.merge_error).to include(error_message)
- expect(Gitlab::AppLogger).to have_received(:error).with(a_string_matching(error_message))
+ expect(merge_request.merge_error).to include(described_class::GENERIC_ERROR_MESSAGE)
+ expect(Gitlab::AppLogger).to have_received(:error).with(a_string_matching(described_class::GENERIC_ERROR_MESSAGE))
end
context 'when squashing is required' do
diff --git a/spec/services/merge_requests/mergeability_check_service_spec.rb b/spec/services/merge_requests/mergeability_check_service_spec.rb
index 17bfa9d7368..e0baf5af8b4 100644
--- a/spec/services/merge_requests/mergeability_check_service_spec.rb
+++ b/spec/services/merge_requests/mergeability_check_service_spec.rb
@@ -33,6 +33,14 @@ RSpec.describe MergeRequests::MergeabilityCheckService, :clean_gitlab_redis_shar
expect(merge_request.merge_status).to eq('can_be_merged')
end
+ it 'reloads merge head diff' do
+ expect_next_instance_of(MergeRequests::ReloadMergeHeadDiffService) do |service|
+ expect(service).to receive(:execute)
+ end
+
+ subject
+ end
+
it 'update diff discussion positions' do
expect_next_instance_of(Discussions::CaptureDiffNotePositionsService) do |service|
expect(service).to receive(:execute)
@@ -142,7 +150,11 @@ RSpec.describe MergeRequests::MergeabilityCheckService, :clean_gitlab_redis_shar
end
it 'resets one merge request upon execution' do
- expect_any_instance_of(MergeRequest).to receive(:reset).once
+ expect_next_instance_of(MergeRequests::ReloadMergeHeadDiffService) do |svc|
+ expect(svc).to receive(:execute).and_return(status: :success)
+ end
+
+ expect_any_instance_of(MergeRequest).to receive(:reset).once.and_call_original
execute_within_threads(amount: 2)
end
@@ -266,6 +278,14 @@ RSpec.describe MergeRequests::MergeabilityCheckService, :clean_gitlab_redis_shar
it_behaves_like 'unmergeable merge request'
+ it 'reloads merge head diff' do
+ expect_next_instance_of(MergeRequests::ReloadMergeHeadDiffService) do |service|
+ expect(service).to receive(:execute)
+ end
+
+ subject
+ end
+
it 'returns ServiceResponse.error' do
result = subject
@@ -329,6 +349,12 @@ RSpec.describe MergeRequests::MergeabilityCheckService, :clean_gitlab_redis_shar
subject
end
+
+ it 'does not reload merge head diff' do
+ expect(MergeRequests::ReloadMergeHeadDiffService).not_to receive(:new)
+
+ subject
+ end
end
end
diff --git a/spec/services/merge_requests/post_merge_service_spec.rb b/spec/services/merge_requests/post_merge_service_spec.rb
index 6523b5a158c..71329905558 100644
--- a/spec/services/merge_requests/post_merge_service_spec.rb
+++ b/spec/services/merge_requests/post_merge_service_spec.rb
@@ -3,9 +3,11 @@
require 'spec_helper'
RSpec.describe MergeRequests::PostMergeService do
- let(:user) { create(:user) }
- let(:merge_request) { create(:merge_request, assignees: [user]) }
- let(:project) { merge_request.project }
+ include ProjectForksHelper
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:merge_request, reload: true) { create(:merge_request, assignees: [user]) }
+ let_it_be(:project) { merge_request.project }
subject { described_class.new(project, user).execute(merge_request) }
@@ -128,5 +130,139 @@ RSpec.describe MergeRequests::PostMergeService do
expect(deploy_job.reload.canceled?).to be false
end
end
+
+ context 'for a merge request chain' do
+ before do
+ ::MergeRequests::UpdateService
+ .new(project, user, force_remove_source_branch: '1')
+ .execute(merge_request)
+ end
+
+ context 'when there is another MR' do
+ let!(:another_merge_request) do
+ create(:merge_request,
+ source_project: source_project,
+ source_branch: 'my-awesome-feature',
+ target_project: merge_request.source_project,
+ target_branch: merge_request.source_branch
+ )
+ end
+
+ shared_examples 'does not retarget merge request' do
+ it 'another merge request is unchanged' do
+ expect { subject }.not_to change { another_merge_request.reload.target_branch }
+ .from(merge_request.source_branch)
+ end
+ end
+
+ shared_examples 'retargets merge request' do
+ it 'another merge request is retargeted' do
+ expect(SystemNoteService)
+ .to receive(:change_branch).once
+ .with(another_merge_request, another_merge_request.project, user,
+ 'target', 'delete',
+ merge_request.source_branch, merge_request.target_branch)
+
+ expect { subject }.to change { another_merge_request.reload.target_branch }
+ .from(merge_request.source_branch)
+ .to(merge_request.target_branch)
+ end
+
+ context 'when FF retarget_merge_requests is disabled' do
+ before do
+ stub_feature_flags(retarget_merge_requests: false)
+ end
+
+ include_examples 'does not retarget merge request'
+ end
+
+ context 'when source branch is to be kept' do
+ before do
+ ::MergeRequests::UpdateService
+ .new(project, user, force_remove_source_branch: false)
+ .execute(merge_request)
+ end
+
+ include_examples 'does not retarget merge request'
+ end
+ end
+
+ context 'in the same project' do
+ let(:source_project) { project }
+
+ it_behaves_like 'retargets merge request'
+
+ context 'and is closed' do
+ before do
+ another_merge_request.close
+ end
+
+ it_behaves_like 'does not retarget merge request'
+ end
+
+ context 'and is merged' do
+ before do
+ another_merge_request.mark_as_merged
+ end
+
+ it_behaves_like 'does not retarget merge request'
+ end
+ end
+
+ context 'in forked project' do
+ let!(:source_project) { fork_project(project) }
+
+ context 'when user has access to source project' do
+ before do
+ source_project.add_developer(user)
+ end
+
+ it_behaves_like 'retargets merge request'
+ end
+
+ context 'when user does not have access to source project' do
+ it_behaves_like 'does not retarget merge request'
+ end
+ end
+
+ context 'and current and another MR is from a fork' do
+ let(:project) { create(:project) }
+ let(:source_project) { fork_project(project) }
+
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: source_project,
+ target_project: project
+ )
+ end
+
+ before do
+ source_project.add_developer(user)
+ end
+
+ it_behaves_like 'does not retarget merge request'
+ end
+ end
+
+ context 'when many merge requests are to be retargeted' do
+ let!(:many_merge_requests) do
+ create_list(:merge_request, 10, :unique_branches,
+ source_project: merge_request.source_project,
+ target_project: merge_request.source_project,
+ target_branch: merge_request.source_branch
+ )
+ end
+
+ it 'retargets only 4 of them' do
+ subject
+
+ expect(many_merge_requests.each(&:reload).pluck(:target_branch).tally)
+ .to eq(
+ merge_request.source_branch => 6,
+ merge_request.target_branch => 4
+ )
+ end
+ end
+ end
end
end
diff --git a/spec/services/merge_requests/push_options_handler_service_spec.rb b/spec/services/merge_requests/push_options_handler_service_spec.rb
index 85bcf4562b1..c2769d4fa88 100644
--- a/spec/services/merge_requests/push_options_handler_service_spec.rb
+++ b/spec/services/merge_requests/push_options_handler_service_spec.rb
@@ -21,6 +21,7 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do
let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" }
let(:deleted_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 #{Gitlab::Git::BLANK_SHA} refs/heads/#{source_branch}" }
let(:default_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{project.default_branch}" }
+ let(:error_mr_required) { "A merge_request.create push option is required to create a merge request for branch #{source_branch}" }
shared_examples_for 'a service that can create a merge request' do
subject(:last_mr) { MergeRequest.last }
@@ -176,11 +177,9 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do
it_behaves_like 'a service that does not create a merge request'
it 'adds an error to the service' do
- error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}"
-
service.execute
- expect(service.errors).to include(error)
+ expect(service.errors).to include(error_mr_required)
end
context 'when coupled with the `create` push option' do
@@ -197,11 +196,9 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do
it_behaves_like 'a service that does not create a merge request'
it 'adds an error to the service' do
- error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}"
-
service.execute
- expect(service.errors).to include(error)
+ expect(service.errors).to include(error_mr_required)
end
context 'when coupled with the `create` push option' do
@@ -263,11 +260,9 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do
it_behaves_like 'a service that does not create a merge request'
it 'adds an error to the service' do
- error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}"
-
service.execute
- expect(service.errors).to include(error)
+ expect(service.errors).to include(error_mr_required)
end
context 'when coupled with the `create` push option' do
@@ -308,11 +303,9 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do
it_behaves_like 'a service that does not create a merge request'
it 'adds an error to the service' do
- error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}"
-
service.execute
- expect(service.errors).to include(error)
+ expect(service.errors).to include(error_mr_required)
end
context 'when coupled with the `create` push option' do
@@ -329,11 +322,9 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do
it_behaves_like 'a service that does not create a merge request'
it 'adds an error to the service' do
- error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}"
-
service.execute
- expect(service.errors).to include(error)
+ expect(service.errors).to include(error_mr_required)
end
context 'when coupled with the `create` push option' do
@@ -374,11 +365,9 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do
it_behaves_like 'a service that does not create a merge request'
it 'adds an error to the service' do
- error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}"
-
service.execute
- expect(service.errors).to include(error)
+ expect(service.errors).to include(error_mr_required)
end
context 'when coupled with the `create` push option' do
@@ -395,11 +384,9 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do
it_behaves_like 'a service that does not create a merge request'
it 'adds an error to the service' do
- error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}"
-
service.execute
- expect(service.errors).to include(error)
+ expect(service.errors).to include(error_mr_required)
end
context 'when coupled with the `create` push option' do
@@ -440,11 +427,9 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do
it_behaves_like 'a service that does not create a merge request'
it 'adds an error to the service' do
- error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}"
-
service.execute
- expect(service.errors).to include(error)
+ expect(service.errors).to include(error_mr_required)
end
context 'when coupled with the `create` push option' do
@@ -461,11 +446,9 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do
it_behaves_like 'a service that does not create a merge request'
it 'adds an error to the service' do
- error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}"
-
service.execute
- expect(service.errors).to include(error)
+ expect(service.errors).to include(error_mr_required)
end
context 'when coupled with the `create` push option' do
@@ -506,11 +489,9 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do
it_behaves_like 'a service that does not create a merge request'
it 'adds an error to the service' do
- error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}"
-
service.execute
- expect(service.errors).to include(error)
+ expect(service.errors).to include(error_mr_required)
end
context 'when coupled with the `create` push option' do
@@ -527,11 +508,9 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do
it_behaves_like 'a service that does not create a merge request'
it 'adds an error to the service' do
- error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}"
-
service.execute
- expect(service.errors).to include(error)
+ expect(service.errors).to include(error_mr_required)
end
context 'when coupled with the `create` push option' do
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 3ccf02fcdfb..747ecbf4fa4 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -633,31 +633,37 @@ RSpec.describe MergeRequests::RefreshService do
end
context 'merge request metrics' do
- let(:issue) { create :issue, project: @project }
- let(:commit_author) { create :user }
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:issue) { create(:issue, project: project) }
let(:commit) { project.commit }
before do
- project.add_developer(commit_author)
project.add_developer(user)
allow(commit).to receive_messages(
safe_message: "Closes #{issue.to_reference}",
references: [issue],
- author_name: commit_author.name,
- author_email: commit_author.email,
+ author_name: user.name,
+ author_email: user.email,
committed_date: Time.current
)
-
- allow_any_instance_of(MergeRequest).to receive(:commits).and_return(CommitCollection.new(@project, [commit], 'feature'))
end
context 'when the merge request is sourced from the same project' do
it 'creates a `MergeRequestsClosingIssues` record for each issue closed by a commit' do
- merge_request = create(:merge_request, target_branch: 'master', source_branch: 'feature', source_project: @project)
- refresh_service = service.new(@project, @user)
+ allow_any_instance_of(MergeRequest).to receive(:commits).and_return(
+ CommitCollection.new(project, [commit], 'close-by-commit')
+ )
+
+ merge_request = create(:merge_request,
+ target_branch: 'master',
+ source_branch: 'close-by-commit',
+ source_project: project)
+
+ refresh_service = service.new(project, user)
allow(refresh_service).to receive(:execute_hooks)
- refresh_service.execute(@oldrev, @newrev, 'refs/heads/feature')
+ refresh_service.execute(@oldrev, @newrev, 'refs/heads/close-by-commit')
issue_ids = MergeRequestsClosingIssues.where(merge_request: merge_request).pluck(:issue_id)
expect(issue_ids).to eq([issue.id])
@@ -666,16 +672,21 @@ RSpec.describe MergeRequests::RefreshService do
context 'when the merge request is sourced from a different project' do
it 'creates a `MergeRequestsClosingIssues` record for each issue closed by a commit' do
- forked_project = fork_project(@project, @user, repository: true)
+ forked_project = fork_project(project, user, repository: true)
+
+ allow_any_instance_of(MergeRequest).to receive(:commits).and_return(
+ CommitCollection.new(forked_project, [commit], 'close-by-commit')
+ )
merge_request = create(:merge_request,
target_branch: 'master',
- source_branch: 'feature',
- target_project: @project,
+ target_project: project,
+ source_branch: 'close-by-commit',
source_project: forked_project)
- refresh_service = service.new(@project, @user)
+
+ refresh_service = service.new(forked_project, user)
allow(refresh_service).to receive(:execute_hooks)
- refresh_service.execute(@oldrev, @newrev, 'refs/heads/feature')
+ refresh_service.execute(@oldrev, @newrev, 'refs/heads/close-by-commit')
issue_ids = MergeRequestsClosingIssues.where(merge_request: merge_request).pluck(:issue_id)
expect(issue_ids).to eq([issue.id])
diff --git a/spec/services/merge_requests/reload_merge_head_diff_service_spec.rb b/spec/services/merge_requests/reload_merge_head_diff_service_spec.rb
new file mode 100644
index 00000000000..3152a4e3861
--- /dev/null
+++ b/spec/services/merge_requests/reload_merge_head_diff_service_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe MergeRequests::ReloadMergeHeadDiffService do
+ let(:merge_request) { create(:merge_request) }
+
+ subject { described_class.new(merge_request).execute }
+
+ describe '#execute' do
+ before do
+ MergeRequests::MergeToRefService
+ .new(merge_request.project, merge_request.author)
+ .execute(merge_request)
+ end
+
+ it 'creates a merge head diff' do
+ expect(subject[:status]).to eq(:success)
+ expect(merge_request.reload.merge_head_diff).to be_present
+ end
+
+ context 'when merge ref head is not present' do
+ before do
+ allow(merge_request).to receive(:merge_ref_head).and_return(nil)
+ end
+
+ it 'returns error' do
+ expect(subject[:status]).to eq(:error)
+ end
+ end
+
+ context 'when failed to create merge head diff' do
+ before do
+ allow(merge_request).to receive(:create_merge_head_diff!).and_raise('fail')
+ end
+
+ it 'returns error' do
+ expect(subject[:status]).to eq(:error)
+ end
+ end
+
+ context 'when there is existing merge head diff' do
+ let!(:existing_merge_head_diff) { create(:merge_request_diff, :merge_head, merge_request: merge_request) }
+
+ it 'recreates merge head diff' do
+ expect(subject[:status]).to eq(:success)
+ expect(merge_request.reload.merge_head_diff).not_to eq(existing_merge_head_diff)
+ end
+ end
+
+ context 'when default_merge_ref_for_diffs feature flag is disabled' do
+ before do
+ stub_feature_flags(default_merge_ref_for_diffs: false)
+ end
+
+ it 'returns error' do
+ expect(subject[:status]).to eq(:error)
+ 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 40da928e832..4ef2da290e1 100644
--- a/spec/services/merge_requests/remove_approval_service_spec.rb
+++ b/spec/services/merge_requests/remove_approval_service_spec.rb
@@ -32,6 +32,13 @@ RSpec.describe MergeRequests::RemoveApprovalService do
execute!
end
+
+ it 'tracks merge request unapprove action' do
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to receive(:track_unapprove_mr_action).with(user: user)
+
+ execute!
+ end
end
context 'with a user who has not approved' do
@@ -41,6 +48,13 @@ RSpec.describe MergeRequests::RemoveApprovalService do
execute!
end
+
+ it 'does not track merge request unapprove action' do
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .not_to receive(:track_unapprove_mr_action).with(user: user)
+
+ execute!
+ end
end
end
end
diff --git a/spec/services/merge_requests/request_review_service_spec.rb b/spec/services/merge_requests/request_review_service_spec.rb
new file mode 100644
index 00000000000..5cb4120852a
--- /dev/null
+++ b/spec/services/merge_requests/request_review_service_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe MergeRequests::RequestReviewService do
+ let(:current_user) { create(:user) }
+ let(:user) { create(:user) }
+ let(:merge_request) { create(:merge_request, reviewers: [user]) }
+ let(:reviewer) { merge_request.find_reviewer(user) }
+ let(:project) { merge_request.project }
+ let(:service) { described_class.new(project, current_user) }
+ let(:result) { service.execute(merge_request, user) }
+ let(:todo_service) { spy('todo service') }
+ let(:notification_service) { spy('notification service') }
+
+ before do
+ allow(NotificationService).to receive(:new) { notification_service }
+ allow(service).to receive(:todo_service).and_return(todo_service)
+ allow(service).to receive(:notification_service).and_return(notification_service)
+
+ reviewer.update!(state: MergeRequestReviewer.states[:reviewed])
+
+ project.add_developer(current_user)
+ project.add_developer(user)
+ end
+
+ describe '#execute' do
+ describe 'invalid permissions' do
+ let(:service) { described_class.new(project, create(:user)) }
+
+ it 'returns an error' do
+ expect(result[:status]).to eq :error
+ end
+ end
+
+ describe 'reviewer does not exist' do
+ let(:result) { service.execute(merge_request, create(:user)) }
+
+ it 'returns an error' do
+ expect(result[:status]).to eq :error
+ end
+ end
+
+ describe 'reviewer exists' do
+ it 'returns success' do
+ expect(result[:status]).to eq :success
+ end
+
+ it 'updates reviewers state' do
+ service.execute(merge_request, user)
+ reviewer.reload
+
+ expect(reviewer.state).to eq 'unreviewed'
+ end
+
+ it 'sends email to reviewer' do
+ expect(notification_service).to receive_message_chain(:async, :review_requested_of_merge_request).with(merge_request, current_user, user)
+
+ service.execute(merge_request, user)
+ end
+
+ it 'creates a new todo for the reviewer' do
+ expect(todo_service).to receive(:create_request_review_todo).with(merge_request, current_user, user)
+
+ service.execute(merge_request, user)
+ end
+ end
+ end
+end
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 9eb82dcd0ad..edb95840604 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -87,6 +87,38 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
expect(@merge_request.discussion_locked).to be_truthy
end
+ context 'usage counters' do
+ let(:merge_request2) { create(:merge_request) }
+ let(:draft_merge_request) { create(:merge_request, :draft_merge_request)}
+
+ it 'update as expected' do
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to receive(:track_title_edit_action).once.with(user: user)
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to receive(:track_description_edit_action).once.with(user: user)
+
+ MergeRequests::UpdateService.new(project, user, opts).execute(merge_request2)
+ end
+
+ it 'tracks Draft/WIP marking' do
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to receive(:track_marked_as_draft_action).once.with(user: user)
+
+ opts[:title] = "WIP: #{opts[:title]}"
+
+ MergeRequests::UpdateService.new(project, user, opts).execute(merge_request2)
+ end
+
+ it 'tracks Draft/WIP un-marking' do
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to receive(:track_unmarked_as_draft_action).once.with(user: user)
+
+ opts[:title] = "Non-draft/wip title string"
+
+ MergeRequests::UpdateService.new(project, user, opts).execute(draft_merge_request)
+ end
+ end
+
context 'updating milestone' do
RSpec.shared_examples 'updates milestone' do
it 'sets milestone' do
@@ -142,29 +174,19 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
context 'with reviewers' do
let(:opts) { { reviewer_ids: [user2.id] } }
- context 'when merge_request_reviewers feature is disabled' do
- before(:context) do
- stub_feature_flags(merge_request_reviewers: false)
- end
-
- it 'does not create a system note about merge_request review request' do
- note = find_note('review requested from')
+ it 'creates system note about merge_request review request' do
+ note = find_note('requested review from')
- expect(note).to be_nil
- end
+ expect(note).not_to be_nil
+ expect(note.note).to include "requested review from #{user2.to_reference}"
end
- context 'when merge_request_reviewers feature is enabled' do
- before(:context) do
- stub_feature_flags(merge_request_reviewers: true)
- end
-
- it 'creates system note about merge_request review request' do
- note = find_note('requested review from')
+ it 'updates the tracking' do
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to receive(:track_users_review_requested)
+ .with(users: [user])
- expect(note).not_to be_nil
- expect(note.note).to include "requested review from #{user2.to_reference}"
- end
+ update_merge_request(reviewer_ids: [user.id])
end
end
@@ -794,6 +816,14 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
expect(merge_request.assignee_ids).to eq([user.id])
end
+ it 'updates the tracking when user ids are valid' do
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to receive(:track_users_assigned_to_mr)
+ .with(users: [user])
+
+ update_merge_request(assignee_ids: [user.id])
+ end
+
it 'does not update assignee_id when user cannot read issue' do
non_member = create(:user)
original_assignees = merge_request.assignees
@@ -883,6 +913,33 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
end
end
+ context 'updating `target_branch`' do
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: project,
+ source_branch: 'mr-b',
+ target_branch: 'mr-a')
+ end
+
+ it 'updates to master' do
+ expect(SystemNoteService).to receive(:change_branch).with(
+ merge_request, project, user, 'target', 'update', 'mr-a', 'master'
+ )
+
+ expect { update_merge_request(target_branch: 'master') }
+ .to change { merge_request.reload.target_branch }.from('mr-a').to('master')
+ end
+
+ it 'updates to master because of branch deletion' do
+ expect(SystemNoteService).to receive(:change_branch).with(
+ merge_request, project, user, 'target', 'delete', 'mr-a', 'master'
+ )
+
+ expect { update_merge_request(target_branch: 'master', target_branch_was_deleted: true) }
+ .to change { merge_request.reload.target_branch }.from('mr-a').to('master')
+ end
+ end
+
it_behaves_like 'issuable record that supports quick actions' do
let(:existing_merge_request) { create(:merge_request, source_project: project) }
let(:issuable) { described_class.new(project, user, params).execute(existing_merge_request) }
diff --git a/spec/services/namespaces/in_product_marketing_emails_service_spec.rb b/spec/services/namespaces/in_product_marketing_emails_service_spec.rb
new file mode 100644
index 00000000000..7346a5b95ae
--- /dev/null
+++ b/spec/services/namespaces/in_product_marketing_emails_service_spec.rb
@@ -0,0 +1,159 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Namespaces::InProductMarketingEmailsService, '#execute' do
+ subject(:execute_service) { described_class.new(track, interval).execute }
+
+ let(:track) { :create }
+ let(:interval) { 1 }
+
+ let(:previous_action_completed_at) { 2.days.ago.middle_of_day }
+ let(:current_action_completed_at) { nil }
+ let(:experiment_enabled) { true }
+ let(:user_can_perform_current_track_action) { true }
+ let(:actions_completed) { { created_at: previous_action_completed_at, git_write_at: current_action_completed_at } }
+
+ let_it_be(:group) { create(:group) }
+ let_it_be(:user) { create(:user, email_opted_in: true) }
+
+ before do
+ create(:onboarding_progress, namespace: group, **actions_completed)
+ group.add_developer(user)
+ stub_experiment_for_subject(in_product_marketing_emails: experiment_enabled)
+ allow(Ability).to receive(:allowed?).with(user, anything, anything).and_return(user_can_perform_current_track_action)
+ allow(Notify).to receive(:in_product_marketing_email).and_return(double(deliver_later: nil))
+ end
+
+ RSpec::Matchers.define :send_in_product_marketing_email do |*args|
+ match do
+ expect(Notify).to have_received(:in_product_marketing_email).with(*args).once
+ end
+
+ match_when_negated do
+ expect(Notify).not_to have_received(:in_product_marketing_email)
+ end
+ end
+
+ context 'for each track and series with the right conditions' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:track, :interval, :actions_completed) do
+ :create | 1 | { created_at: 2.days.ago.middle_of_day }
+ :create | 5 | { created_at: 6.days.ago.middle_of_day }
+ :create | 10 | { created_at: 11.days.ago.middle_of_day }
+ :verify | 1 | { created_at: 2.days.ago.middle_of_day, git_write_at: 2.days.ago.middle_of_day }
+ :verify | 5 | { created_at: 6.days.ago.middle_of_day, git_write_at: 6.days.ago.middle_of_day }
+ :verify | 10 | { created_at: 11.days.ago.middle_of_day, git_write_at: 11.days.ago.middle_of_day }
+ :trial | 1 | { created_at: 2.days.ago.middle_of_day, git_write_at: 2.days.ago.middle_of_day, pipeline_created_at: 2.days.ago.middle_of_day }
+ :trial | 5 | { created_at: 6.days.ago.middle_of_day, git_write_at: 6.days.ago.middle_of_day, pipeline_created_at: 6.days.ago.middle_of_day }
+ :trial | 10 | { created_at: 11.days.ago.middle_of_day, git_write_at: 11.days.ago.middle_of_day, pipeline_created_at: 11.days.ago.middle_of_day }
+ :team | 1 | { created_at: 2.days.ago.middle_of_day, git_write_at: 2.days.ago.middle_of_day, pipeline_created_at: 2.days.ago.middle_of_day, trial_started_at: 2.days.ago.middle_of_day }
+ :team | 5 | { created_at: 6.days.ago.middle_of_day, git_write_at: 6.days.ago.middle_of_day, pipeline_created_at: 6.days.ago.middle_of_day, trial_started_at: 6.days.ago.middle_of_day }
+ :team | 10 | { created_at: 11.days.ago.middle_of_day, git_write_at: 11.days.ago.middle_of_day, pipeline_created_at: 11.days.ago.middle_of_day, trial_started_at: 11.days.ago.middle_of_day }
+ end
+
+ with_them do
+ it { is_expected.to send_in_product_marketing_email(user.id, group.id, track, described_class::INTERVAL_DAYS.index(interval)) }
+ end
+ end
+
+ context 'when initialized with a different track' do
+ let(:track) { :verify }
+
+ it { is_expected.not_to send_in_product_marketing_email }
+
+ context 'when the previous track actions have been completed' do
+ let(:current_action_completed_at) { 2.days.ago.middle_of_day }
+
+ it { is_expected.to send_in_product_marketing_email(user.id, group.id, :verify, 0) }
+ end
+ end
+
+ context 'when initialized with a different interval' do
+ let(:interval) { 5 }
+
+ it { is_expected.not_to send_in_product_marketing_email }
+
+ context 'when the previous track action was completed within the intervals range' do
+ let(:previous_action_completed_at) { 6.days.ago.middle_of_day }
+
+ it { is_expected.to send_in_product_marketing_email(user.id, group.id, :create, 1) }
+ end
+ end
+
+ describe 'experimentation' do
+ context 'when the experiment is enabled' do
+ it 'adds the group as an experiment subject in the experimental group' do
+ expect(Experiment).to receive(:add_group)
+ .with(:in_product_marketing_emails, variant: :experimental, group: group)
+
+ execute_service
+ end
+ end
+
+ context 'when the experiment is disabled' do
+ let(:experiment_enabled) { false }
+
+ it 'adds the group as an experiment subject in the control group' do
+ expect(Experiment).to receive(:add_group)
+ .with(:in_product_marketing_emails, variant: :control, group: group)
+
+ execute_service
+ end
+
+ it { is_expected.not_to send_in_product_marketing_email }
+ end
+ end
+
+ context 'when the previous track action is not yet completed' do
+ let(:previous_action_completed_at) { nil }
+
+ it { is_expected.not_to send_in_product_marketing_email }
+ end
+
+ context 'when the previous track action is completed outside the intervals range' do
+ let(:previous_action_completed_at) { 3.days.ago }
+
+ it { is_expected.not_to send_in_product_marketing_email }
+ end
+
+ context 'when the current track action is completed' do
+ let(:current_action_completed_at) { Time.current }
+
+ it { is_expected.not_to send_in_product_marketing_email }
+ end
+
+ context "when the user cannot perform the current track's action" do
+ let(:user_can_perform_current_track_action) { false }
+
+ it { is_expected.not_to send_in_product_marketing_email }
+ end
+
+ context 'when the user has not opted into marketing emails' do
+ let(:user) { create(:user, email_opted_in: false) }
+
+ it { is_expected.not_to send_in_product_marketing_email }
+ end
+
+ context 'when the user has already received a marketing email as part of another group' do
+ before do
+ other_group = create(:group)
+ other_group.add_developer(user)
+ create(:onboarding_progress, namespace: other_group, created_at: previous_action_completed_at, git_write_at: current_action_completed_at)
+ end
+
+ # For any group Notify is called exactly once
+ it { is_expected.to send_in_product_marketing_email(user.id, anything, :create, 0) }
+ end
+
+ context 'when invoked with a non existing track' do
+ let(:track) { :foo }
+
+ before do
+ stub_const("#{described_class}::TRACKS", { foo: :git_write })
+ end
+
+ it { expect { subject }.to raise_error(NotImplementedError, 'No ability defined for track foo') }
+ end
+end
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index 20f06619e02..f59749f0b63 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -459,6 +459,26 @@ RSpec.describe Notes::CreateService do
.and change { existing_note.updated_at }
end
+ context 'failure in when_saved' do
+ let(:service) { described_class.new(project, user, reply_opts) }
+
+ it 'converts existing note to DiscussionNote' do
+ expect do
+ existing_note
+
+ allow(service).to receive(:when_saved).and_raise(ActiveRecord::StatementInvalid)
+
+ travel_to(Time.current + 1.minute) do
+ service.execute
+ rescue ActiveRecord::StatementInvalid
+ end
+
+ existing_note.reload
+ end.to change { existing_note.type }.from(nil).to('DiscussionNote')
+ .and change { existing_note.updated_at }
+ end
+ end
+
it 'returns a DiscussionNote with its parent discussion refreshed correctly' do
discussion_notes = subject.discussion.notes
diff --git a/spec/services/notification_recipients/build_service_spec.rb b/spec/services/notification_recipients/build_service_spec.rb
index cc08f9fceff..ff54d6ccd2f 100644
--- a/spec/services/notification_recipients/build_service_spec.rb
+++ b/spec/services/notification_recipients/build_service_spec.rb
@@ -110,4 +110,28 @@ RSpec.describe NotificationRecipients::BuildService do
end
end
end
+
+ describe '#build_requested_review_recipients' do
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+
+ before do
+ merge_request.reviewers.push(assignee)
+ end
+
+ shared_examples 'no N+1 queries' do
+ it 'avoids N+1 queries', :request_store do
+ create_user
+
+ service.build_requested_review_recipients(note)
+
+ control_count = ActiveRecord::QueryRecorder.new do
+ service.build_requested_review_recipients(note)
+ end
+
+ create_user
+
+ expect { service.build_requested_review_recipients(note) }.not_to exceed_query_limit(control_count)
+ end
+ end
+ end
end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 85234077b1f..b67c37ba02d 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -2177,6 +2177,46 @@ RSpec.describe NotificationService, :mailer do
let(:notification_trigger) { notification.merge_when_pipeline_succeeds(merge_request, @u_disabled) }
end
end
+
+ describe '#review_requested_of_merge_request' do
+ let(:merge_request) { create(:merge_request, author: author, source_project: project, reviewers: [reviewer]) }
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:reviewer) { create(:user) }
+
+ it 'sends email to reviewer', :aggregate_failures do
+ notification.review_requested_of_merge_request(merge_request, current_user, reviewer)
+
+ merge_request.reviewers.each { |reviewer| should_email(reviewer) }
+ should_not_email(merge_request.author)
+ should_not_email(@u_watcher)
+ should_not_email(@u_participant_mentioned)
+ should_not_email(@subscriber)
+ should_not_email(@watcher_and_subscriber)
+ should_not_email(@u_guest_watcher)
+ should_not_email(@u_guest_custom)
+ should_not_email(@u_custom_global)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
+ end
+
+ it 'adds "review requested" reason for new reviewer' do
+ notification.review_requested_of_merge_request(merge_request, current_user, [reviewer])
+
+ merge_request.reviewers.each do |reviewer|
+ email = find_email_for(reviewer)
+
+ expect(email).to have_header('X-GitLab-NotificationReason', NotificationReason::REVIEW_REQUESTED)
+ end
+ end
+
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { merge_request }
+ let(:notification_trigger) { notification.review_requested_of_merge_request(merge_request, current_user, reviewer) }
+ end
+ end
end
describe 'Projects', :deliver_mails_inline do
diff --git a/spec/services/packages/composer/create_package_service_spec.rb b/spec/services/packages/composer/create_package_service_spec.rb
index d10356cfda7..4f1a46e7e45 100644
--- a/spec/services/packages/composer/create_package_service_spec.rb
+++ b/spec/services/packages/composer/create_package_service_spec.rb
@@ -43,6 +43,7 @@ RSpec.describe Packages::Composer::CreatePackageService do
end
it_behaves_like 'assigns build to package'
+ it_behaves_like 'assigns status to package'
end
context 'with a tag' do
@@ -66,6 +67,7 @@ RSpec.describe Packages::Composer::CreatePackageService do
end
it_behaves_like 'assigns build to package'
+ it_behaves_like 'assigns status to package'
end
end
diff --git a/spec/services/packages/conan/create_package_service_spec.rb b/spec/services/packages/conan/create_package_service_spec.rb
index ca783475503..6f644f5ef95 100644
--- a/spec/services/packages/conan/create_package_service_spec.rb
+++ b/spec/services/packages/conan/create_package_service_spec.rb
@@ -31,6 +31,7 @@ RSpec.describe Packages::Conan::CreatePackageService do
it_behaves_like 'assigns the package creator'
it_behaves_like 'assigns build to package'
+ it_behaves_like 'assigns status to package'
end
context 'invalid params' do
diff --git a/spec/services/packages/debian/create_distribution_service_spec.rb b/spec/services/packages/debian/create_distribution_service_spec.rb
new file mode 100644
index 00000000000..87cf1070075
--- /dev/null
+++ b/spec/services/packages/debian/create_distribution_service_spec.rb
@@ -0,0 +1,122 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Debian::CreateDistributionService do
+ RSpec.shared_examples 'Create Debian Distribution' do |expected_message, expected_components, expected_architectures|
+ it 'returns ServiceResponse', :aggregate_failures do
+ if expected_message.nil?
+ expect { response }
+ .to change { container.debian_distributions.klass.all.count }
+ .from(0).to(1)
+ .and change { container.debian_distributions.count }
+ .from(0).to(1)
+ .and change { container.debian_distributions.first&.components&.count }
+ .from(nil).to(expected_components.count)
+ .and change { container.debian_distributions.first&.architectures&.count }
+ .from(nil).to(expected_architectures.count)
+ .and not_change { Packages::Debian::ProjectComponentFile.count }
+ .and not_change { Packages::Debian::GroupComponentFile.count }
+ else
+ expect { response }
+ .to not_change { container.debian_distributions.klass.all.count }
+ .and not_change { container.debian_distributions.count }
+ .and not_change { Packages::Debian::ProjectComponent.count }
+ .and not_change { Packages::Debian::GroupComponent.count }
+ .and not_change { Packages::Debian::ProjectArchitecture.count }
+ .and not_change { Packages::Debian::GroupArchitecture.count }
+ .and not_change { Packages::Debian::ProjectComponentFile.count }
+ .and not_change { Packages::Debian::GroupComponentFile.count }
+ end
+
+ expect(response).to be_a(ServiceResponse)
+ expect(response.success?).to eq(expected_message.nil?)
+ expect(response.error?).to eq(!expected_message.nil?)
+ expect(response.message).to eq(expected_message)
+
+ distribution = response.payload[:distribution]
+ expect(distribution.persisted?).to eq(expected_message.nil?)
+ expect(distribution.container).to eq(container)
+ expect(distribution.creator).to eq(user)
+ params.each_pair do |name, value|
+ expect(distribution.send(name)).to eq(value)
+ end
+
+ expect(distribution.components.map(&:name)).to contain_exactly(*expected_components)
+ expect(distribution.architectures.map(&:name)).to contain_exactly(*expected_architectures)
+ end
+ end
+
+ shared_examples 'Debian Create Distribution Service' do
+ context 'with only the codename param' do
+ let(:params) { { codename: 'my-codename' } }
+
+ it_behaves_like 'Create Debian Distribution', nil, %w[main], %w[all amd64]
+ end
+
+ context 'with codename, components and architectures' do
+ let(:params) do
+ {
+ codename: 'my-codename',
+ components: %w[contrib non-free],
+ architectures: %w[arm64]
+ }
+ end
+
+ it_behaves_like 'Create Debian Distribution', nil, %w[contrib non-free], %w[all arm64]
+ end
+
+ context 'with invalid suite' do
+ let(:params) do
+ {
+ codename: 'my-codename',
+ suite: 'erroné'
+ }
+ end
+
+ it_behaves_like 'Create Debian Distribution', 'Suite is invalid', %w[], %w[]
+ end
+
+ context 'with invalid component name' do
+ let(:params) do
+ {
+ codename: 'my-codename',
+ components: %w[before erroné after],
+ architectures: %w[arm64]
+ }
+ end
+
+ it_behaves_like 'Create Debian Distribution', 'Component Name is invalid', %w[before erroné], %w[]
+ end
+
+ context 'with invalid architecture name' do
+ let(:params) do
+ {
+ codename: 'my-codename',
+ components: %w[contrib non-free],
+ architectures: %w[before erroné after']
+ }
+ end
+
+ it_behaves_like 'Create Debian Distribution', 'Architecture Name is invalid', %w[contrib non-free], %w[before erroné]
+ end
+ end
+
+ let_it_be(:user) { create(:user) }
+
+ subject { described_class.new(container, user, params) }
+
+ let(:response) { subject.execute }
+
+ context 'within a projet' do
+ let_it_be(:container) { create(:project) }
+
+ it_behaves_like 'Debian Create Distribution Service'
+ end
+
+ context 'within a group' do
+ let_it_be(:container) { create(:group) }
+
+ it_behaves_like 'Debian Create Distribution Service'
+ end
+end
diff --git a/spec/services/packages/debian/destroy_distribution_service_spec.rb b/spec/services/packages/debian/destroy_distribution_service_spec.rb
new file mode 100644
index 00000000000..e4c43884bb4
--- /dev/null
+++ b/spec/services/packages/debian/destroy_distribution_service_spec.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Debian::DestroyDistributionService do
+ RSpec.shared_examples 'Destroy Debian Distribution' do |expected_message|
+ it 'returns ServiceResponse', :aggregate_failures do
+ if expected_message.nil?
+ expect { response }
+ .to change { container.debian_distributions.klass.all.count }
+ .from(1).to(0)
+ .and change { container.debian_distributions.count }
+ .from(1).to(0)
+ .and change { component1.class.all.count }
+ .from(2).to(0)
+ .and change { architecture1.class.all.count }
+ .from(3).to(0)
+ .and change { component_file1.class.all.count }
+ .from(4).to(0)
+ else
+ expect { response }
+ .to not_change { container.debian_distributions.klass.all.count }
+ .and not_change { container.debian_distributions.count }
+ .and not_change { component1.class.all.count }
+ .and not_change { architecture1.class.all.count }
+ .and not_change { component_file1.class.all.count }
+ end
+
+ expect(response).to be_a(ServiceResponse)
+ expect(response.success?).to eq(expected_message.nil?)
+ expect(response.error?).to eq(!expected_message.nil?)
+ expect(response.message).to eq(expected_message)
+
+ if expected_message.nil?
+ expect(response.payload).to eq({})
+ else
+ expect(response.payload).to eq(distribution: distribution)
+ end
+ end
+ end
+
+ RSpec.shared_examples 'Debian Destroy Distribution Service' do |container_type, can_freeze|
+ context "with a Debian #{container_type} distribution" do
+ let_it_be(:container, freeze: can_freeze) { create(container_type) } # rubocop:disable Rails/SaveBang
+ let_it_be(:distribution, freeze: can_freeze) { create("debian_#{container_type}_distribution", container: container) }
+ let_it_be(:component1, freeze: can_freeze) { create("debian_#{container_type}_component", distribution: distribution, name: 'component1') }
+ let_it_be(:component2, freeze: can_freeze) { create("debian_#{container_type}_component", distribution: distribution, name: 'component2') }
+ let_it_be(:architecture0, freeze: true) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'all') }
+ let_it_be(:architecture1, freeze: can_freeze) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'architecture1') }
+ let_it_be(:architecture2, freeze: can_freeze) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'architecture2') }
+ let_it_be(:component_file1, freeze: can_freeze) { create("debian_#{container_type}_component_file", :source, component: component1) }
+ let_it_be(:component_file2, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1, architecture: architecture1) }
+ let_it_be(:component_file3, freeze: can_freeze) { create("debian_#{container_type}_component_file", :source, component: component2) }
+ let_it_be(:component_file4, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component2, architecture: architecture2) }
+
+ subject { described_class.new(distribution) }
+
+ let(:response) { subject.execute }
+
+ context 'with a distribution' do
+ it_behaves_like 'Destroy Debian Distribution'
+ end
+
+ context 'when destroy fails' do
+ let(:distribution) { create("debian_#{container_type}_distribution", container: container) }
+
+ before do
+ expect(distribution).to receive(:destroy).and_return(false)
+ end
+
+ it_behaves_like 'Destroy Debian Distribution', "Unable to destroy Debian #{container_type} distribution"
+ end
+ end
+ end
+
+ it_behaves_like 'Debian Destroy Distribution Service', :project, true
+ it_behaves_like 'Debian Destroy Distribution Service', :group, false
+end
diff --git a/spec/services/packages/debian/update_distribution_service_spec.rb b/spec/services/packages/debian/update_distribution_service_spec.rb
new file mode 100644
index 00000000000..852fc713e34
--- /dev/null
+++ b/spec/services/packages/debian/update_distribution_service_spec.rb
@@ -0,0 +1,159 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Debian::UpdateDistributionService do
+ RSpec.shared_examples 'Update Debian Distribution' do |expected_message, expected_components, expected_architectures, component_file_delta = 0|
+ it 'returns ServiceResponse', :aggregate_failures do
+ expect(distribution).to receive(:update).with(simple_params).and_call_original if expected_message.nil?
+
+ if component_file_delta.zero?
+ expect { response }
+ .to not_change { container.debian_distributions.klass.all.count }
+ .and not_change { container.debian_distributions.count }
+ .and not_change { component1.class.all.count }
+ .and not_change { architecture1.class.all.count }
+ .and not_change { component_file1.class.all.count }
+ else
+ expect { response }
+ .to not_change { container.debian_distributions.klass.all.count }
+ .and not_change { container.debian_distributions.count }
+ .and not_change { component1.class.all.count }
+ .and not_change { architecture1.class.all.count }
+ .and change { component_file1.class.all.count }
+ .from(4).to(4 + component_file_delta)
+ end
+
+ expect(response).to be_a(ServiceResponse)
+ expect(response.success?).to eq(expected_message.nil?)
+ expect(response.error?).to eq(!expected_message.nil?)
+ expect(response.message).to eq(expected_message)
+
+ expect(response.payload).to eq(distribution: distribution)
+
+ distribution.reload
+ distribution.components.reload
+ distribution.architectures.reload
+
+ if expected_message.nil?
+ simple_params.each_pair do |name, value|
+ expect(distribution.send(name)).to eq(value)
+ end
+ else
+ original_params.each_pair do |name, value|
+ expect(distribution.send(name)).to eq(value)
+ end
+ end
+
+ expect(distribution.components.map(&:name)).to contain_exactly(*expected_components)
+ expect(distribution.architectures.map(&:name)).to contain_exactly(*expected_architectures)
+ end
+ end
+
+ RSpec.shared_examples 'Debian Update Distribution Service' do |container_type, can_freeze|
+ context "with a Debian #{container_type} distribution" do
+ let_it_be(:container, freeze: can_freeze) { create(container_type) } # rubocop:disable Rails/SaveBang
+ let_it_be(:distribution, reload: true) { create("debian_#{container_type}_distribution", container: container) }
+ let_it_be(:component1) { create("debian_#{container_type}_component", distribution: distribution, name: 'component1') }
+ let_it_be(:component2) { create("debian_#{container_type}_component", distribution: distribution, name: 'component2') }
+ let_it_be(:architecture0) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'all') }
+ let_it_be(:architecture1) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'architecture1') }
+ let_it_be(:architecture2) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'architecture2') }
+ let_it_be(:component_file1) { create("debian_#{container_type}_component_file", :source, component: component1) }
+ let_it_be(:component_file2) { create("debian_#{container_type}_component_file", component: component1, architecture: architecture1) }
+ let_it_be(:component_file3) { create("debian_#{container_type}_component_file", :source, component: component2) }
+ let_it_be(:component_file4) { create("debian_#{container_type}_component_file", component: component2, architecture: architecture2) }
+
+ let(:original_params) do
+ {
+ suite: nil,
+ origin: nil,
+ label: nil,
+ version: nil,
+ description: nil,
+ valid_time_duration_seconds: nil,
+ automatic: true,
+ automatic_upgrades: false
+ }
+ end
+
+ let(:params) { {} }
+ let(:simple_params) { params.except(:components, :architectures) }
+
+ subject { described_class.new(distribution, params) }
+
+ let(:response) { subject.execute }
+
+ context 'with valid simple params' do
+ let(:params) do
+ {
+ suite: 'my-suite',
+ origin: 'my-origin',
+ label: 'my-label',
+ version: '42.0',
+ description: 'my-description',
+ valid_time_duration_seconds: 7.days,
+ automatic: false,
+ automatic_upgrades: true
+ }
+ end
+
+ it_behaves_like 'Update Debian Distribution', nil, %w[component1 component2], %w[all architecture1 architecture2]
+ end
+
+ context 'with invalid simple params' do
+ let(:params) do
+ {
+ suite: 'suite erronée',
+ origin: 'origin erronée',
+ label: 'label erronée',
+ version: 'version erronée',
+ description: 'description erronée',
+ valid_time_duration_seconds: 1.hour
+ }
+ end
+
+ it_behaves_like 'Update Debian Distribution', 'Suite is invalid, Origin is invalid, Label is invalid, Version is invalid, and Valid time duration seconds must be greater than or equal to 86400', %w[component1 component2], %w[all architecture1 architecture2]
+ end
+
+ context 'with valid components and architectures' do
+ let(:params) do
+ {
+ suite: 'my-suite',
+ components: %w[component2 component3],
+ architectures: %w[architecture2 architecture3]
+ }
+ end
+
+ it_behaves_like 'Update Debian Distribution', nil, %w[component2 component3], %w[all architecture2 architecture3], -2
+ end
+
+ context 'with invalid components' do
+ let(:params) do
+ {
+ suite: 'my-suite',
+ components: %w[component2 erroné],
+ architectures: %w[architecture2 architecture3]
+ }
+ end
+
+ it_behaves_like 'Update Debian Distribution', 'Component Name is invalid', %w[component1 component2], %w[all architecture1 architecture2]
+ end
+
+ context 'with invalid architectures' do
+ let(:params) do
+ {
+ suite: 'my-suite',
+ components: %w[component2 component3],
+ architectures: %w[architecture2 erroné]
+ }
+ end
+
+ it_behaves_like 'Update Debian Distribution', 'Architecture Name is invalid', %w[component1 component2], %w[all architecture1 architecture2]
+ end
+ end
+ end
+
+ it_behaves_like 'Debian Update Distribution Service', :project, true
+ it_behaves_like 'Debian Update Distribution Service', :group, false
+end
diff --git a/spec/services/packages/generic/create_package_file_service_spec.rb b/spec/services/packages/generic/create_package_file_service_spec.rb
index 816e728c342..10c54369f26 100644
--- a/spec/services/packages/generic/create_package_file_service_spec.rb
+++ b/spec/services/packages/generic/create_package_file_service_spec.rb
@@ -13,6 +13,8 @@ RSpec.describe Packages::Generic::CreatePackageFileService do
let(:temp_file) { Tempfile.new("test") }
let(:file) { UploadedFile.new(temp_file.path, sha256: sha256) }
let(:package) { create(:generic_package, project: project) }
+ let(:package_service) { double }
+
let(:params) do
{
package_name: 'mypackage',
@@ -23,31 +25,34 @@ RSpec.describe Packages::Generic::CreatePackageFileService do
}
end
+ let(:package_params) do
+ {
+ name: params[:package_name],
+ version: params[:package_version],
+ build: params[:build],
+ status: nil
+ }
+ end
+
subject { described_class.new(project, user, params).execute }
before do
FileUtils.touch(temp_file)
+ expect(::Packages::Generic::FindOrCreatePackageService).to receive(:new).with(project, user, package_params).and_return(package_service)
+ expect(package_service).to receive(:execute).and_return(package)
end
after do
FileUtils.rm_f(temp_file)
end
- it 'creates package file' do
- package_service = double
- package_params = {
- name: params[:package_name],
- version: params[:package_version],
- build: params[:build]
- }
- expect(::Packages::Generic::FindOrCreatePackageService).to receive(:new).with(project, user, package_params).and_return(package_service)
- expect(package_service).to receive(:execute).and_return(package)
-
+ it 'creates package file', :aggregate_failures do
expect { subject }.to change { package.package_files.count }.by(1)
.and change { Packages::PackageFileBuildInfo.count }.by(1)
package_file = package.package_files.last
aggregate_failures do
+ expect(package_file.package.status).to eq('default')
expect(package_file.package).to eq(package)
expect(package_file.file_name).to eq('myfile.tar.gz.1')
expect(package_file.size).to eq(file.size)
@@ -55,6 +60,21 @@ RSpec.describe Packages::Generic::CreatePackageFileService do
end
end
+ context 'with a status' do
+ let(:params) { super().merge(status: 'hidden') }
+ let(:package_params) { super().merge(status: 'hidden') }
+
+ it 'updates an existing packages status' do
+ expect { subject }.to change { package.package_files.count }.by(1)
+ .and change { Packages::PackageFileBuildInfo.count }.by(1)
+
+ package_file = package.package_files.last
+ aggregate_failures do
+ expect(package_file.package.status).to eq('hidden')
+ end
+ end
+ end
+
it_behaves_like 'assigns build to package file'
end
end
diff --git a/spec/services/packages/maven/find_or_create_package_service_spec.rb b/spec/services/packages/maven/find_or_create_package_service_spec.rb
index 82dffeefcde..2543ab0c669 100644
--- a/spec/services/packages/maven/find_or_create_package_service_spec.rb
+++ b/spec/services/packages/maven/find_or_create_package_service_spec.rb
@@ -36,10 +36,11 @@ RSpec.describe Packages::Maven::FindOrCreatePackageService do
expect(pkg.version).to eq(version)
end
- context 'with a build' do
+ context 'with optional attributes' do
subject { service.execute.payload[:package] }
it_behaves_like 'assigns build to package'
+ it_behaves_like 'assigns status to package'
end
end
@@ -111,6 +112,13 @@ RSpec.describe Packages::Maven::FindOrCreatePackageService do
expect(subject.errors).to include('Duplicate package is not allowed')
end
+ context 'when uploading to the versionless package which contains metadata about all versions' do
+ let(:version) { nil }
+ let(:param_path) { path }
+
+ it_behaves_like 'reuse existing package'
+ end
+
context 'when uploading different non-duplicate files to the same package' do
before do
package_file = existing_package.package_files.find_by(file_name: 'my-app-1.0-20180724.124855-1.jar')
diff --git a/spec/services/packages/npm/create_package_service_spec.rb b/spec/services/packages/npm/create_package_service_spec.rb
index 6db3777cde8..10fce6c1651 100644
--- a/spec/services/packages/npm/create_package_service_spec.rb
+++ b/spec/services/packages/npm/create_package_service_spec.rb
@@ -53,6 +53,7 @@ RSpec.describe Packages::Npm::CreatePackageService do
let(:params) { super().merge(build: job) }
it_behaves_like 'assigns build to package'
+ it_behaves_like 'assigns status to package'
it 'creates a package file build info' do
expect { subject }.to change { Packages::PackageFileBuildInfo.count }.by(1)
diff --git a/spec/services/packages/nuget/create_package_service_spec.rb b/spec/services/packages/nuget/create_package_service_spec.rb
index 5289ad40d61..e338ac36fc3 100644
--- a/spec/services/packages/nuget/create_package_service_spec.rb
+++ b/spec/services/packages/nuget/create_package_service_spec.rb
@@ -32,5 +32,6 @@ RSpec.describe Packages::Nuget::CreatePackageService do
it_behaves_like 'assigns the package creator'
it_behaves_like 'assigns build to package'
+ it_behaves_like 'assigns status to package'
end
end
diff --git a/spec/services/packages/pypi/create_package_service_spec.rb b/spec/services/packages/pypi/create_package_service_spec.rb
index 28a727c4a09..a932cf73eb7 100644
--- a/spec/services/packages/pypi/create_package_service_spec.rb
+++ b/spec/services/packages/pypi/create_package_service_spec.rb
@@ -52,6 +52,7 @@ RSpec.describe Packages::Pypi::CreatePackageService do
end
it_behaves_like 'assigns build to package'
+ it_behaves_like 'assigns status to package'
context 'with an existing package' do
before do
diff --git a/spec/services/pages/delete_services_spec.rb b/spec/services/pages/delete_services_spec.rb
index 440549020a2..f1edf93b0c1 100644
--- a/spec/services/pages/delete_services_spec.rb
+++ b/spec/services/pages/delete_services_spec.rb
@@ -3,35 +3,74 @@
require 'spec_helper'
RSpec.describe Pages::DeleteService do
- shared_examples 'remove pages' do
- let_it_be(:project) { create(:project, path: "my.project")}
- let_it_be(:admin) { create(:admin) }
- let_it_be(:domain) { create(:pages_domain, project: project) }
- let_it_be(:service) { described_class.new(project, admin)}
+ let_it_be(:admin) { create(:admin) }
- it 'deletes published pages' do
- expect_any_instance_of(Gitlab::PagesTransfer).to receive(:rename_project).and_return true
- expect(PagesWorker).to receive(:perform_in).with(5.minutes, :remove, project.namespace.full_path, anything)
+ let(:project) { create(:project, path: "my.project")}
+ let!(:domain) { create(:pages_domain, project: project) }
+ let(:service) { described_class.new(project, admin)}
- Sidekiq::Testing.inline! { service.execute }
+ before do
+ project.mark_pages_as_deployed
+ end
+
+ it 'deletes published pages', :sidekiq_inline do
+ expect(project.pages_deployed?).to be(true)
+
+ expect_any_instance_of(Gitlab::PagesTransfer).to receive(:rename_project).and_return true
+ expect(PagesWorker).to receive(:perform_in).with(5.minutes, :remove, project.namespace.full_path, anything)
+
+ service.execute
+
+ expect(project.pages_deployed?).to be(false)
+ end
+
+ it "doesn't remove anything from the legacy storage if updates on it are disabled", :sidekiq_inline do
+ stub_feature_flags(pages_update_legacy_storage: false)
+
+ expect(project.pages_deployed?).to be(true)
+
+ expect(PagesWorker).not_to receive(:perform_in)
+
+ service.execute
- expect(project.reload.pages_metadatum.deployed?).to be(false)
- end
+ expect(project.pages_deployed?).to be(false)
+ end
+
+ it 'deletes all domains', :sidekiq_inline do
+ expect(project.pages_domains.count).to eq(1)
+
+ service.execute
+
+ expect(project.reload.pages_domains.count).to eq(0)
+ end
- it 'deletes all domains' do
- expect(project.pages_domains.count).to be 1
+ it 'schedules a destruction of pages deployments' do
+ expect(DestroyPagesDeploymentsWorker).to(
+ receive(:perform_async).with(project.id)
+ )
- Sidekiq::Testing.inline! { service.execute }
+ service.execute
+ end
+
+ it 'removes pages deployments', :sidekiq_inline do
+ create(:pages_deployment, project: project)
- expect(project.reload.pages_domains.count).to be 0
- end
+ expect do
+ service.execute
+ end.to change { PagesDeployment.count }.by(-1)
end
- context 'with feature flag enabled' do
- before do
- expect(PagesRemoveWorker).to receive(:perform_async).and_call_original
- end
+ it 'marks pages as not deployed, deletes domains and schedules worker to remove pages from disk' do
+ expect(project.pages_deployed?).to eq(true)
+ expect(project.pages_domains.count).to eq(1)
+
+ service.execute
+
+ expect(project.pages_deployed?).to eq(false)
+ expect(project.pages_domains.count).to eq(0)
+
+ expect_any_instance_of(Gitlab::PagesTransfer).to receive(:rename_project).and_return true
- it_behaves_like 'remove pages'
+ Sidekiq::Worker.drain_all
end
end
diff --git a/spec/services/pages/migrate_from_legacy_storage_service_spec.rb b/spec/services/pages/migrate_from_legacy_storage_service_spec.rb
new file mode 100644
index 00000000000..4ec57044912
--- /dev/null
+++ b/spec/services/pages/migrate_from_legacy_storage_service_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Pages::MigrateFromLegacyStorageService do
+ let(:service) { described_class.new(Rails.logger, migration_threads: 3, batch_size: 10, ignore_invalid_entries: false) }
+
+ it 'does not try to migrate pages if pages are not deployed' do
+ expect(::Pages::MigrateLegacyStorageToDeploymentService).not_to receive(:new)
+
+ expect(service.execute).to eq(migrated: 0, errored: 0)
+ end
+
+ it 'uses multiple threads' do
+ projects = create_list(:project, 20)
+ projects.each do |project|
+ project.mark_pages_as_deployed
+
+ FileUtils.mkdir_p File.join(project.pages_path, "public")
+ File.open(File.join(project.pages_path, "public/index.html"), "w") do |f|
+ f.write("Hello!")
+ end
+ end
+
+ service = described_class.new(Rails.logger, migration_threads: 3, batch_size: 2, ignore_invalid_entries: false)
+
+ threads = Concurrent::Set.new
+
+ expect(service).to receive(:migrate_project).exactly(20).times.and_wrap_original do |m, *args|
+ threads.add(Thread.current)
+
+ # sleep to be 100% certain that once thread can't consume all the queue
+ # it works without it, but I want to avoid making this test flaky
+ sleep(0.01)
+
+ m.call(*args)
+ end
+
+ expect(service.execute).to eq(migrated: 20, errored: 0)
+ expect(threads.length).to eq(3)
+ end
+
+ context 'when pages are marked as deployed' do
+ let(:project) { create(:project) }
+
+ before do
+ project.mark_pages_as_deployed
+ end
+
+ context 'when pages directory does not exist' do
+ it 'tries to migrate the project, but does not crash' do
+ expect_next_instance_of(::Pages::MigrateLegacyStorageToDeploymentService, project, ignore_invalid_entries: false) do |service|
+ expect(service).to receive(:execute).and_call_original
+ end
+
+ expect(service.execute).to eq(migrated: 0, errored: 1)
+ end
+ end
+
+ context 'when pages directory exists on disk' do
+ before do
+ FileUtils.mkdir_p File.join(project.pages_path, "public")
+ File.open(File.join(project.pages_path, "public/index.html"), "w") do |f|
+ f.write("Hello!")
+ end
+ end
+
+ it 'migrates pages projects without deployments' do
+ expect_next_instance_of(::Pages::MigrateLegacyStorageToDeploymentService, project, ignore_invalid_entries: false) do |service|
+ expect(service).to receive(:execute).and_call_original
+ end
+
+ expect do
+ expect(service.execute).to eq(migrated: 1, errored: 0)
+ end.to change { project.pages_metadatum.reload.pages_deployment }.from(nil)
+ end
+
+ context 'when deployed already exists for the project' do
+ before do
+ deployment = create(:pages_deployment, project: project)
+ project.set_first_pages_deployment!(deployment)
+ end
+
+ it 'does not try to migrate project' do
+ expect(::Pages::MigrateLegacyStorageToDeploymentService).not_to receive(:new)
+
+ expect(service.execute).to eq(migrated: 0, errored: 0)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/pages/migrate_legacy_storage_to_deployment_service_spec.rb b/spec/services/pages/migrate_legacy_storage_to_deployment_service_spec.rb
index 29023621413..d95303c3e85 100644
--- a/spec/services/pages/migrate_legacy_storage_to_deployment_service_spec.rb
+++ b/spec/services/pages/migrate_legacy_storage_to_deployment_service_spec.rb
@@ -6,6 +6,14 @@ RSpec.describe Pages::MigrateLegacyStorageToDeploymentService do
let(:project) { create(:project, :repository) }
let(:service) { described_class.new(project) }
+ it 'calls ::Pages::ZipDirectoryService' do
+ expect_next_instance_of(::Pages::ZipDirectoryService, project.pages_path, ignore_invalid_entries: true) do |zip_service|
+ expect(zip_service).to receive(:execute).and_call_original
+ end
+
+ expect(described_class.new(project, ignore_invalid_entries: true).execute[:status]).to eq(:error)
+ end
+
it 'marks pages as not deployed if public directory is absent' do
project.mark_pages_as_deployed
diff --git a/spec/services/pages/zip_directory_service_spec.rb b/spec/services/pages/zip_directory_service_spec.rb
index dcab6b2dada..9de68dd62bb 100644
--- a/spec/services/pages/zip_directory_service_spec.rb
+++ b/spec/services/pages/zip_directory_service_spec.rb
@@ -10,8 +10,14 @@ RSpec.describe Pages::ZipDirectoryService do
end
end
+ let(:ignore_invalid_entries) { false }
+
+ let(:service) do
+ described_class.new(@work_dir, ignore_invalid_entries: ignore_invalid_entries)
+ end
+
let(:result) do
- described_class.new(@work_dir).execute
+ service.execute
end
let(:status) { result[:status] }
@@ -20,6 +26,8 @@ RSpec.describe Pages::ZipDirectoryService do
let(:entries_count) { result[:entries_count] }
it 'returns error if project pages dir does not exist' do
+ expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
+
expect(
described_class.new("/tmp/not/existing/dir").execute
).to eq(status: :error, message: "Can not find valid public dir in /tmp/not/existing/dir")
@@ -132,32 +140,69 @@ RSpec.describe Pages::ZipDirectoryService do
end
end
- it 'ignores the symlink pointing outside of public directory' do
- create_file("target.html", "hello")
- create_link("public/link.html", "../target.html")
+ shared_examples "raises or ignores file" do |raised_exception, file|
+ it 'raises error' do
+ expect do
+ result
+ end.to raise_error(raised_exception)
+ end
- with_zip_file do |zip_file|
- expect { zip_file.get_entry("public/link.html") }.to raise_error(Errno::ENOENT)
+ context 'when errors are ignored' do
+ let(:ignore_invalid_entries) { true }
+
+ it 'does not create entry' do
+ with_zip_file do |zip_file|
+ expect { zip_file.get_entry(file) }.to raise_error(Errno::ENOENT)
+ end
+ end
end
end
- it 'ignores the symlink if target is absent' do
- create_link("public/link.html", "./target.html")
+ context 'when symlink points outside of public directory' do
+ before do
+ create_file("target.html", "hello")
+ create_link("public/link.html", "../target.html")
+ end
- with_zip_file do |zip_file|
- expect { zip_file.get_entry("public/link.html") }.to raise_error(Errno::ENOENT)
+ include_examples "raises or ignores file", described_class::InvalidEntryError, "public/link.html"
+ end
+
+ context 'when target of the symlink is absent' do
+ before do
+ create_link("public/link.html", "./target.html")
end
+
+ include_examples "raises or ignores file", Errno::ENOENT, "public/link.html"
end
- it 'ignores symlink if is absolute and points to outside of directory' do
- target = File.join(@work_dir, "target")
- FileUtils.touch(target)
+ context 'when targets itself' do
+ before do
+ create_link("public/link.html", "./link.html")
+ end
- create_link("public/link.html", target)
+ include_examples "raises or ignores file", Errno::ELOOP, "public/link.html"
+ end
- with_zip_file do |zip_file|
- expect { zip_file.get_entry("public/link.html") }.to raise_error(Errno::ENOENT)
+ context 'when symlink is absolute and points to outside of directory' do
+ before do
+ target = File.join(@work_dir, "target")
+ FileUtils.touch(target)
+
+ create_link("public/link.html", target)
end
+
+ include_examples "raises or ignores file", described_class::InvalidEntryError, "public/link.html"
+ end
+
+ context 'when entry has unknown ftype' do
+ before do
+ file = create_file("public/index.html", "hello")
+
+ allow(File).to receive(:lstat).and_call_original
+ expect(File).to receive(:lstat).with(file) { double("lstat", ftype: "unknown") }
+ end
+
+ include_examples "raises or ignores file", described_class::InvalidEntryError, "public/index.html"
end
it "includes raw symlink if it's target is a valid directory" do
@@ -204,9 +249,13 @@ RSpec.describe Pages::ZipDirectoryService do
end
def create_file(name, content)
- File.open(File.join(@work_dir, name), "w") do |f|
+ file_path = File.join(@work_dir, name)
+
+ File.open(file_path, "w") do |f|
f.write(content)
end
+
+ file_path
end
def create_dir(dir)
diff --git a/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb b/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb
index 4d489d7fe4b..79654c9b190 100644
--- a/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb
+++ b/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb
@@ -135,7 +135,7 @@ RSpec.describe PagesDomains::ObtainLetsEncryptCertificateService do
cert.add_extension ef.create_extension("authorityKeyIdentifier",
"keyid:always,issuer:always")
- cert.sign key, OpenSSL::Digest::SHA1.new
+ cert.sign key, OpenSSL::Digest.new('SHA1')
cert.to_pem
end
diff --git a/spec/services/post_receive_service_spec.rb b/spec/services/post_receive_service_spec.rb
index 6e2cd7edf04..033194972c7 100644
--- a/spec/services/post_receive_service_spec.rb
+++ b/spec/services/post_receive_service_spec.rb
@@ -4,7 +4,6 @@ require 'spec_helper'
RSpec.describe PostReceiveService do
include Gitlab::Routing
- include AfterNextHelpers
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository, :wiki_repo, namespace: user.namespace) }
@@ -47,11 +46,7 @@ RSpec.describe PostReceiveService do
expect(subject).to be_empty
end
- it 'does not record an onboarding progress action' do
- expect_next(OnboardingProgressService).not_to receive(:execute)
-
- subject
- end
+ it_behaves_like 'does not record an onboarding progress action'
end
context 'when repository is nil' do
@@ -88,11 +83,8 @@ RSpec.describe PostReceiveService do
expect(response.reference_counter_decreased).to be(true)
end
- it 'records an onboarding progress action' do
- expect_next(OnboardingProgressService, project.namespace)
- .to receive(:execute).with(action: :git_write)
-
- subject
+ it_behaves_like 'records an onboarding progress action', :git_write do
+ let(:namespace) { project.namespace }
end
end
diff --git a/spec/services/projects/alerting/notify_service_spec.rb b/spec/services/projects/alerting/notify_service_spec.rb
index 4b7b7b0b200..4e366fce0d9 100644
--- a/spec/services/projects/alerting/notify_service_spec.rb
+++ b/spec/services/projects/alerting/notify_service_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe Projects::Alerting::NotifyService do
subject { service.execute(token, nil) }
- shared_examples 'notifcations are handled correctly' do
+ shared_examples 'notifications are handled correctly' do
context 'with valid token' do
let(:token) { integration.token }
let(:incident_management_setting) { double(send_email?: email_enabled, create_issue?: issue_enabled, auto_close_incident?: auto_close_enabled) }
@@ -85,6 +85,15 @@ RSpec.describe Projects::Alerting::NotifyService do
it_behaves_like 'creates an alert management alert'
it_behaves_like 'assigns the alert properties'
+ it 'passes the integration to alert processing' do
+ expect(Gitlab::AlertManagement::Payload)
+ .to receive(:parse)
+ .with(project, payload.to_h, integration: integration)
+ .and_call_original
+
+ subject
+ end
+
it 'creates a system note corresponding to alert creation' do
expect { subject }.to change(Note, :count).by(1)
expect(Note.last.note).to include(payload_raw.fetch(:monitoring_tool))
@@ -259,7 +268,7 @@ RSpec.describe Projects::Alerting::NotifyService do
subject { service.execute(token, integration) }
- it_behaves_like 'notifcations are handled correctly' do
+ it_behaves_like 'notifications are handled correctly' do
let(:source) { integration.name }
end
diff --git a/spec/services/projects/branches_by_mode_service_spec.rb b/spec/services/projects/branches_by_mode_service_spec.rb
new file mode 100644
index 00000000000..9199c3e0b3a
--- /dev/null
+++ b/spec/services/projects/branches_by_mode_service_spec.rb
@@ -0,0 +1,136 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::BranchesByModeService do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :repository) }
+
+ let(:finder) { described_class.new(project, params) }
+ let(:params) { { mode: 'all' } }
+
+ subject { finder.execute }
+
+ describe '#execute' do
+ context 'page is passed' do
+ let(:params) { { page: 4, mode: 'all', offset: 3 } }
+
+ it 'uses offset pagination' do
+ expect(finder).to receive(:fetch_branches_via_offset_pagination).and_call_original
+
+ branches, prev_page, next_page = subject
+
+ expect(branches.size).to eq(10)
+ expect(next_page).to be_nil
+ expect(prev_page).to eq("/#{project.full_path}/-/branches/all?offset=2&page=3")
+ end
+
+ context 'but the page does not contain any branches' do
+ let(:params) { { page: 10, mode: 'all' } }
+
+ it 'uses offset pagination' do
+ expect(finder).to receive(:fetch_branches_via_offset_pagination).and_call_original
+
+ branches, prev_page, next_page = subject
+
+ expect(branches).to eq([])
+ expect(next_page).to be_nil
+ expect(prev_page).to be_nil
+ end
+ end
+ end
+
+ context 'search is passed' do
+ let(:params) { { search: 'feature' } }
+
+ it 'uses offset pagination' do
+ expect(finder).to receive(:fetch_branches_via_offset_pagination).and_call_original
+
+ branches, prev_page, next_page = subject
+
+ expect(branches.map(&:name)).to match_array(%w(feature feature_conflict))
+ expect(next_page).to be_nil
+ expect(prev_page).to be_nil
+ end
+ end
+
+ context 'branch_list_keyset_pagination is disabled' do
+ it 'uses offset pagination' do
+ stub_feature_flags(branch_list_keyset_pagination: false)
+
+ expect(finder).to receive(:fetch_branches_via_offset_pagination).and_call_original
+
+ branches, prev_page, next_page = subject
+
+ expect(branches.size).to eq(20)
+ expect(next_page).to eq("/#{project.full_path}/-/branches/all?offset=1&page_token=conflict-resolvable")
+ expect(prev_page).to be_nil
+ end
+ end
+
+ context 'uses gitaly pagination' do
+ before do
+ expect(finder).to receive(:fetch_branches_via_gitaly_pagination).and_call_original
+ end
+
+ it 'returns branches for the first page' do
+ branches, prev_page, next_page = subject
+
+ expect(branches.size).to eq(20)
+ expect(next_page).to eq("/#{project.full_path}/-/branches/all?offset=1&page_token=conflict-resolvable")
+ expect(prev_page).to be_nil
+ end
+
+ context 'when second page is requested' do
+ let(:params) { { page_token: 'conflict-resolvable', mode: 'all', sort: 'name_asc', offset: 1 } }
+
+ it 'returns branches for the first page' do
+ branches, prev_page, next_page = subject
+
+ expect(branches.size).to eq(20)
+ expect(next_page).to eq("/#{project.full_path}/-/branches/all?offset=2&page_token=improve%2Fawesome&sort=name_asc")
+ expect(prev_page).to eq("/#{project.full_path}/-/branches/all?offset=0&page=1&sort=name_asc")
+ end
+ end
+
+ context 'when last page is requested' do
+ let(:params) { { page_token: 'signed-commits', mode: 'all', sort: 'name_asc', offset: 4 } }
+
+ it 'returns branches after the specified branch' do
+ branches, prev_page, next_page = subject
+
+ expect(branches.size).to eq(14)
+ expect(next_page).to be_nil
+ expect(prev_page).to eq("/#{project.full_path}/-/branches/all?offset=3&page=4&sort=name_asc")
+ end
+ end
+ end
+
+ context 'filter by mode' do
+ let(:stale) { double(state: 'stale') }
+ let(:active) { double(state: 'active') }
+
+ before do
+ allow_next_instance_of(BranchesFinder) do |instance|
+ allow(instance).to receive(:execute).and_return([stale, active])
+ end
+ end
+
+ context 'stale' do
+ let(:params) { { mode: 'stale' } }
+
+ it 'returns stale branches' do
+ is_expected.to eq([[stale], nil, nil])
+ end
+ end
+
+ context 'active' do
+ let(:params) { { mode: 'active' } }
+
+ it 'returns active branches' do
+ is_expected.to eq([[active], nil, nil])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/projects/cleanup_service_spec.rb b/spec/services/projects/cleanup_service_spec.rb
index 6fd29813d98..f2c052d9397 100644
--- a/spec/services/projects/cleanup_service_spec.rb
+++ b/spec/services/projects/cleanup_service_spec.rb
@@ -88,7 +88,7 @@ RSpec.describe Projects::CleanupService do
end
it 'runs garbage collection on the repository' do
- expect_next_instance_of(GitGarbageCollectWorker) do |worker|
+ expect_next_instance_of(Projects::GitGarbageCollectWorker) do |worker|
expect(worker).to receive(:perform).with(project.id, :prune, "project_cleanup:gc:#{project.id}")
end
diff --git a/spec/services/projects/container_repository/cleanup_tags_service_spec.rb b/spec/services/projects/container_repository/cleanup_tags_service_spec.rb
index 17c2f0f6c17..eed22416868 100644
--- a/spec/services/projects/container_repository/cleanup_tags_service_spec.rb
+++ b/spec/services/projects/container_repository/cleanup_tags_service_spec.rb
@@ -284,7 +284,7 @@ RSpec.describe Projects::ContainerRepository::CleanupTagsService do
deleted: nil
)
- expect(result).to eq(service_response.compact)
+ expect(result).to eq(service_response)
end
end
@@ -369,6 +369,6 @@ RSpec.describe Projects::ContainerRepository::CleanupTagsService do
before_truncate_size: before_truncate_size,
after_truncate_size: after_truncate_size,
before_delete_size: before_delete_size
- }.compact
+ }.compact.merge(deleted_size: deleted&.size)
end
end
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index 6c0e6654622..f7da6f75141 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -40,6 +40,48 @@ RSpec.describe Projects::CreateService, '#execute' do
end
end
+ describe 'setting name and path' do
+ subject(:project) { create_project(user, opts) }
+
+ context 'when both are set' do
+ let(:opts) { { name: 'one', path: 'two' } }
+
+ it 'keeps them as specified' do
+ expect(project.name).to eq('one')
+ expect(project.path).to eq('two')
+ end
+ end
+
+ context 'when path is set' do
+ let(:opts) { { path: 'one.two_three-four' } }
+
+ it 'sets name == path' do
+ expect(project.path).to eq('one.two_three-four')
+ expect(project.name).to eq(project.path)
+ end
+ end
+
+ context 'when name is a valid path' do
+ let(:opts) { { name: 'one.two_three-four' } }
+
+ it 'sets path == name' do
+ expect(project.name).to eq('one.two_three-four')
+ expect(project.path).to eq(project.name)
+ end
+ end
+
+ context 'when name is not a valid path' do
+ let(:opts) { { name: 'one.two_three-four and five' } }
+
+ # TODO: Retained for backwards compatibility. Remove in API v5.
+ # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52725
+ it 'parameterizes the name' do
+ expect(project.name).to eq('one.two_three-four and five')
+ expect(project.path).to eq('one-two_three-four-and-five')
+ end
+ end
+ end
+
context 'user namespace' do
it do
project = create_project(user, opts)
@@ -419,7 +461,7 @@ RSpec.describe Projects::CreateService, '#execute' do
context 'when another repository already exists on disk' do
let(:opts) do
{
- name: 'Existing',
+ name: 'existing',
namespace_id: user.namespace.id
}
end
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index a11f16573f5..df02f8ea15d 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -323,6 +323,50 @@ RSpec.describe Projects::ForkService do
end
end
end
+
+ describe 'fork with optional attributes' do
+ let(:public_project) { create(:project, :public) }
+
+ it 'sets optional attributes to specified values' do
+ forked_project = fork_project(
+ public_project,
+ nil,
+ namespace: public_project.namespace,
+ path: 'forked',
+ name: 'My Fork',
+ description: 'Description',
+ visibility: 'internal',
+ using_service: true
+ )
+
+ expect(forked_project.path).to eq('forked')
+ expect(forked_project.name).to eq('My Fork')
+ expect(forked_project.description).to eq('Description')
+ expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL)
+ end
+
+ it 'sets visibility level to private if an unknown visibility is requested' do
+ forked_project = fork_project(public_project, nil, using_service: true, visibility: 'unknown')
+
+ expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ it 'sets visibility level to project visibility level if requested visibility is greater' do
+ private_project = create(:project, :private)
+
+ forked_project = fork_project(private_project, nil, using_service: true, visibility: 'public')
+
+ expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ it 'sets visibility level to target namespace visibility level if requested visibility is greater' do
+ private_group = create(:group, :private)
+
+ forked_project = fork_project(public_project, nil, namespace: private_group, using_service: true, visibility: 'public')
+
+ expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+ end
end
context 'when a project is already forked' do
diff --git a/spec/services/projects/prometheus/alerts/notify_service_spec.rb b/spec/services/projects/prometheus/alerts/notify_service_spec.rb
index 8ae47ec266c..e196220eabe 100644
--- a/spec/services/projects/prometheus/alerts/notify_service_spec.rb
+++ b/spec/services/projects/prometheus/alerts/notify_service_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe Projects::Prometheus::Alerts::NotifyService do
include PrometheusHelpers
+ using RSpec::Parameterized::TableSyntax
let_it_be(:project, reload: true) { create(:project) }
@@ -61,8 +62,6 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do
end
context 'with project specific cluster' do
- using RSpec::Parameterized::TableSyntax
-
where(:cluster_enabled, :status, :configured_token, :token_input, :result) do
true | :installed | token | token | :success
true | :installed | nil | nil | :success
@@ -104,8 +103,6 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do
end
context 'with manual prometheus installation' do
- using RSpec::Parameterized::TableSyntax
-
where(:alerting_setting, :configured_token, :token_input, :result) do
true | token | token | :success
true | token | 'x' | :failure
@@ -139,8 +136,6 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do
end
context 'with HTTP integration' do
- using RSpec::Parameterized::TableSyntax
-
where(:active, :token, :result) do
:active | :valid | :success
:active | :invalid | :failure
diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb
index a6730c5de52..6bf2876f640 100644
--- a/spec/services/projects/update_pages_service_spec.rb
+++ b/spec/services/projects/update_pages_service_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe Projects::UpdatePagesService do
subject { described_class.new(project, build) }
before do
- project.remove_pages
+ project.legacy_remove_pages
end
context '::TMP_EXTRACT_PATH' do
@@ -55,6 +55,15 @@ RSpec.describe Projects::UpdatePagesService do
end
end
+ it "doesn't deploy to legacy storage if it's disabled" do
+ stub_feature_flags(pages_update_legacy_storage: false)
+
+ expect(execute).to eq(:success)
+ expect(project.pages_deployed?).to be_truthy
+
+ expect(File.exist?(File.join(project.pages_path, 'public', 'index.html'))).to eq(false)
+ end
+
it 'creates pages_deployment and saves it in the metadata' do
expect do
expect(execute).to eq(:success)
diff --git a/spec/services/projects/update_repository_storage_service_spec.rb b/spec/services/projects/update_repository_storage_service_spec.rb
index ef8f166cc3f..828667fdfc2 100644
--- a/spec/services/projects/update_repository_storage_service_spec.rb
+++ b/spec/services/projects/update_repository_storage_service_spec.rb
@@ -59,13 +59,18 @@ RSpec.describe Projects::UpdateRepositoryStorageService do
end
context 'when the filesystems are the same' do
- let(:destination) { project.repository_storage }
+ before do
+ expect(Gitlab::GitalyClient).to receive(:filesystem_id).twice.and_return(SecureRandom.uuid)
+ end
- it 'bails out and does nothing' do
+ it 'updates the database without trying to move the repostory', :aggregate_failures do
result = subject.execute
+ project.reload
- expect(result).to be_error
- expect(result.message).to match(/SameFilesystemError/)
+ expect(result).to be_success
+ expect(project).not_to be_repository_read_only
+ expect(project.repository_storage).to eq('test_second_storage')
+ expect(project.project_repository.shard_name).to eq('test_second_storage')
end
end
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index 21e294418a1..1a102b125f6 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -879,139 +879,123 @@ RSpec.describe QuickActions::InterpretService do
let(:issuable) { issue }
end
- context 'when the merge_request_reviewers flag is enabled' do
- describe 'assign_reviewer command' do
- let(:content) { "/assign_reviewer @#{developer.username}" }
- let(:issuable) { merge_request }
+ describe 'assign_reviewer command' do
+ let(:content) { "/assign_reviewer @#{developer.username}" }
+ let(:issuable) { merge_request }
- context 'with one user' do
- it_behaves_like 'assign_reviewer command'
- end
+ context 'with one user' do
+ it_behaves_like 'assign_reviewer command'
+ end
- context 'with an issue instead of a merge request' do
- let(:issuable) { issue }
+ context 'with an issue instead of a merge request' do
+ let(:issuable) { issue }
- it_behaves_like 'empty command'
- end
+ it_behaves_like 'empty command'
+ end
- # CE does not have multiple reviewers
- context 'assign command with multiple assignees' do
- before do
- project.add_developer(developer2)
- end
+ # CE does not have multiple reviewers
+ context 'assign command with multiple assignees' do
+ before do
+ project.add_developer(developer2)
+ end
- # There's no guarantee that the reference extractor will preserve
- # the order of the mentioned users since this is dependent on the
- # order in which rows are returned. We just ensure that at least
- # one of the mentioned users is assigned.
- context 'assigns to one of the two users' do
- let(:content) { "/assign_reviewer @#{developer.username} @#{developer2.username}" }
+ # There's no guarantee that the reference extractor will preserve
+ # the order of the mentioned users since this is dependent on the
+ # order in which rows are returned. We just ensure that at least
+ # one of the mentioned users is assigned.
+ context 'assigns to one of the two users' do
+ let(:content) { "/assign_reviewer @#{developer.username} @#{developer2.username}" }
- it 'assigns to a single reviewer' do
- _, updates, message = service.execute(content, issuable)
+ it 'assigns to a single reviewer' do
+ _, updates, message = service.execute(content, issuable)
- expect(updates[:reviewer_ids].count).to eq(1)
- reviewer = updates[:reviewer_ids].first
- expect([developer.id, developer2.id]).to include(reviewer)
+ expect(updates[:reviewer_ids].count).to eq(1)
+ reviewer = updates[:reviewer_ids].first
+ expect([developer.id, developer2.id]).to include(reviewer)
- user = reviewer == developer.id ? developer : developer2
+ user = reviewer == developer.id ? developer : developer2
- expect(message).to match("Assigned #{user.to_reference} as reviewer.")
- end
+ expect(message).to match("Assigned #{user.to_reference} as reviewer.")
end
end
+ end
- context 'with "me" alias' do
- let(:content) { '/assign_reviewer me' }
+ context 'with "me" alias' do
+ let(:content) { '/assign_reviewer me' }
- it_behaves_like 'assign_reviewer command'
- end
+ it_behaves_like 'assign_reviewer command'
+ end
- context 'with an alias and whitespace' do
- let(:content) { '/assign_reviewer me ' }
+ context 'with an alias and whitespace' do
+ let(:content) { '/assign_reviewer me ' }
- it_behaves_like 'assign_reviewer command'
- end
+ it_behaves_like 'assign_reviewer command'
+ end
- context 'with an incorrect user' do
- let(:content) { '/assign_reviewer @abcd1234' }
+ context 'with an incorrect user' do
+ let(:content) { '/assign_reviewer @abcd1234' }
- it_behaves_like 'empty command', "Failed to assign a reviewer because no user was found."
- end
+ it_behaves_like 'empty command', "Failed to assign a reviewer because no user was found."
+ end
- context 'with the "reviewer" alias' do
- let(:content) { "/reviewer @#{developer.username}" }
+ context 'with the "reviewer" alias' do
+ let(:content) { "/reviewer @#{developer.username}" }
- it_behaves_like 'assign_reviewer command'
- end
+ it_behaves_like 'assign_reviewer command'
+ end
- context 'with no user' do
- let(:content) { '/assign_reviewer' }
+ context 'with the "request_review" alias' do
+ let(:content) { "/request_review @#{developer.username}" }
- it_behaves_like 'empty command', "Failed to assign a reviewer because no user was found."
- end
+ it_behaves_like 'assign_reviewer command'
+ end
- context 'includes only the user reference with extra text' do
- let(:content) { "/assign_reviewer @#{developer.username} do it!" }
+ context 'with no user' do
+ let(:content) { '/assign_reviewer' }
- it_behaves_like 'assign_reviewer command'
- end
+ it_behaves_like 'empty command', "Failed to assign a reviewer because no user was found."
end
- describe 'unassign_reviewer command' do
- # CE does not have multiple reviewers, so basically anything
- # after /unassign_reviewer (including whitespace) will remove
- # all the current reviewers.
- let(:issuable) { create(:merge_request, reviewers: [developer]) }
- let(:content) { "/unassign_reviewer @#{developer.username}" }
+ context 'includes only the user reference with extra text' do
+ let(:content) { "/assign_reviewer @#{developer.username} do it!" }
- context 'with one user' do
- it_behaves_like 'unassign_reviewer command'
- end
-
- context 'with an issue instead of a merge request' do
- let(:issuable) { issue }
-
- it_behaves_like 'empty command'
- end
+ it_behaves_like 'assign_reviewer command'
+ end
+ end
- context 'with anything after the command' do
- let(:content) { '/unassign_reviewer supercalifragilisticexpialidocious' }
+ describe 'unassign_reviewer command' do
+ # CE does not have multiple reviewers, so basically anything
+ # after /unassign_reviewer (including whitespace) will remove
+ # all the current reviewers.
+ let(:issuable) { create(:merge_request, reviewers: [developer]) }
+ let(:content) { "/unassign_reviewer @#{developer.username}" }
- it_behaves_like 'unassign_reviewer command'
- end
+ context 'with one user' do
+ it_behaves_like 'unassign_reviewer command'
+ end
- context 'with the "remove_reviewer" alias' do
- let(:content) { "/remove_reviewer @#{developer.username}" }
+ context 'with an issue instead of a merge request' do
+ let(:issuable) { issue }
- it_behaves_like 'unassign_reviewer command'
- end
+ it_behaves_like 'empty command'
+ end
- context 'with no user' do
- let(:content) { '/unassign_reviewer' }
+ context 'with anything after the command' do
+ let(:content) { '/unassign_reviewer supercalifragilisticexpialidocious' }
- it_behaves_like 'unassign_reviewer command'
- end
+ it_behaves_like 'unassign_reviewer command'
end
- end
- context 'when the merge_request_reviewers flag is disabled' do
- before do
- stub_feature_flags(merge_request_reviewers: false)
- end
+ context 'with the "remove_reviewer" alias' do
+ let(:content) { "/remove_reviewer @#{developer.username}" }
- describe 'assign_reviewer command' do
- it_behaves_like 'empty command' do
- let(:content) { "/assign_reviewer @#{developer.username}" }
- let(:issuable) { merge_request }
- end
+ it_behaves_like 'unassign_reviewer command'
end
- describe 'unassign_reviewer command' do
- it_behaves_like 'empty command' do
- let(:content) { "/unassign_reviewer @#{developer.username}" }
- let(:issuable) { merge_request }
- end
+ context 'with no user' do
+ let(:content) { '/unassign_reviewer' }
+
+ it_behaves_like 'unassign_reviewer command'
end
end
@@ -1787,6 +1771,24 @@ RSpec.describe QuickActions::InterpretService do
expect(text).to eq(" - list\n\ntest")
end
+ it 'tracks MAU for commands' do
+ content = "/shrug test\n/assign me\n/milestone %4"
+
+ expect(Gitlab::UsageDataCounters::QuickActionActivityUniqueCounter)
+ .to receive(:track_unique_action)
+ .with('shrug', args: 'test', user: developer)
+
+ expect(Gitlab::UsageDataCounters::QuickActionActivityUniqueCounter)
+ .to receive(:track_unique_action)
+ .with('assign', args: 'me', user: developer)
+
+ expect(Gitlab::UsageDataCounters::QuickActionActivityUniqueCounter)
+ .to receive(:track_unique_action)
+ .with('milestone', args: '%4', user: developer)
+
+ service.execute(content, issue)
+ end
+
context '/create_merge_request command' do
let(:branch_name) { '1-feature' }
let(:content) { "/create_merge_request #{branch_name}" }
diff --git a/spec/services/repositories/changelog_service_spec.rb b/spec/services/repositories/changelog_service_spec.rb
new file mode 100644
index 00000000000..a545b0f070a
--- /dev/null
+++ b/spec/services/repositories/changelog_service_spec.rb
@@ -0,0 +1,130 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Repositories::ChangelogService do
+ describe '#execute' do
+ it 'generates and commits a changelog section' do
+ project = create(:project, :empty_repo)
+ creator = project.creator
+ author1 = create(:user)
+ author2 = create(:user)
+
+ project.add_maintainer(author1)
+ project.add_maintainer(author2)
+
+ mr1 = create(:merge_request, :merged, target_project: project)
+ mr2 = create(:merge_request, :merged, target_project: project)
+
+ # The range of commits ignores the first commit, but includes the last
+ # commit. To ensure both the commits below are included, we must create an
+ # extra commit.
+ #
+ # In the real world, the start commit of the range will be the last commit
+ # of the previous release, so ignoring that is expected and desired.
+ sha1 = create_commit(
+ project,
+ creator,
+ commit_message: 'Initial commit',
+ actions: [{ action: 'create', content: 'test', file_path: 'README.md' }]
+ )
+
+ sha2 = create_commit(
+ project,
+ author1,
+ commit_message: "Title 1\n\nChangelog: feature",
+ actions: [{ action: 'create', content: 'foo', file_path: 'a.txt' }]
+ )
+
+ sha3 = create_commit(
+ project,
+ author2,
+ commit_message: "Title 2\n\nChangelog: feature",
+ actions: [{ action: 'create', content: 'bar', file_path: 'b.txt' }]
+ )
+
+ commit1 = project.commit(sha2)
+ commit2 = project.commit(sha3)
+
+ allow(MergeRequestDiffCommit)
+ .to receive(:oldest_merge_request_id_per_commit)
+ .with(project.id, [commit2.id, commit1.id])
+ .and_return([
+ { sha: sha2, merge_request_id: mr1.id },
+ { sha: sha3, merge_request_id: mr2.id }
+ ])
+
+ recorder = ActiveRecord::QueryRecorder.new do
+ described_class
+ .new(project, creator, version: '1.0.0', from: sha1, to: sha3)
+ .execute
+ end
+
+ changelog = project.repository.blob_at('master', 'CHANGELOG.md')&.data
+
+ expect(recorder.count).to eq(10)
+ expect(changelog).to include('Title 1', 'Title 2')
+ end
+ end
+
+ describe '#start_of_commit_range' do
+ let(:project) { build_stubbed(:project) }
+ let(:user) { build_stubbed(:user) }
+
+ context 'when the "from" argument is specified' do
+ it 'returns the value of the argument' do
+ service = described_class
+ .new(project, user, version: '1.0.0', from: 'foo', to: 'bar')
+
+ expect(service.start_of_commit_range).to eq('foo')
+ end
+ end
+
+ context 'when the "from" argument is unspecified' do
+ it 'returns the tag commit of the previous version' do
+ service = described_class
+ .new(project, user, version: '1.0.0', to: 'bar')
+
+ finder_spy = instance_spy(Repositories::PreviousTagFinder)
+ tag = double(:tag, target_commit: double(:commit, id: '123'))
+
+ allow(Repositories::PreviousTagFinder)
+ .to receive(:new)
+ .with(project)
+ .and_return(finder_spy)
+
+ allow(finder_spy)
+ .to receive(:execute)
+ .with('1.0.0')
+ .and_return(tag)
+
+ expect(service.start_of_commit_range).to eq('123')
+ end
+
+ it 'raises an error when no tag is found' do
+ service = described_class
+ .new(project, user, version: '1.0.0', to: 'bar')
+
+ finder_spy = instance_spy(Repositories::PreviousTagFinder)
+
+ allow(Repositories::PreviousTagFinder)
+ .to receive(:new)
+ .with(project)
+ .and_return(finder_spy)
+
+ allow(finder_spy)
+ .to receive(:execute)
+ .with('1.0.0')
+ .and_return(nil)
+
+ expect { service.start_of_commit_range }
+ .to raise_error(Gitlab::Changelog::Error)
+ end
+ end
+ end
+
+ def create_commit(project, user, params)
+ params = { start_branch: 'master', branch_name: 'master' }.merge(params)
+ Files::MultiService.new(project, user, params).execute.fetch(:result)
+ end
+end
diff --git a/spec/services/resource_access_tokens/create_service_spec.rb b/spec/services/resource_access_tokens/create_service_spec.rb
index 5cfa1ae93e6..517ed086713 100644
--- a/spec/services/resource_access_tokens/create_service_spec.rb
+++ b/spec/services/resource_access_tokens/create_service_spec.rb
@@ -195,6 +195,14 @@ RSpec.describe ResourceAccessTokens::CreateService do
end
end
end
+
+ it 'logs the event' do
+ allow(Gitlab::AppLogger).to receive(:info)
+
+ response = subject
+
+ expect(Gitlab::AppLogger).to have_received(:info).with(/PROJECT ACCESS TOKEN CREATION: created_by: #{user.username}, project_id: #{resource.id}, token_user: #{response.payload[:access_token].user.name}, token_id: \d+/)
+ end
end
context 'when resource is a project' do
@@ -208,7 +216,7 @@ RSpec.describe ResourceAccessTokens::CreateService do
response = subject
expect(response.error?).to be true
- expect(response.errors).to include("User does not have permission to create #{resource_type} Access Token")
+ expect(response.errors).to include("User does not have permission to create #{resource_type} access token")
end
end
diff --git a/spec/services/resource_access_tokens/revoke_service_spec.rb b/spec/services/resource_access_tokens/revoke_service_spec.rb
index af29ee2a721..99adb4bb7a0 100644
--- a/spec/services/resource_access_tokens/revoke_service_spec.rb
+++ b/spec/services/resource_access_tokens/revoke_service_spec.rb
@@ -40,6 +40,14 @@ RSpec.describe ResourceAccessTokens::RevokeService do
expect(User.exists?(resource_bot.id)).to be_falsy
end
+
+ it 'logs the event' do
+ allow(Gitlab::AppLogger).to receive(:info)
+
+ subject
+
+ expect(Gitlab::AppLogger).to have_received(:info).with("PROJECT ACCESS TOKEN REVOCATION: revoked_by: #{user.username}, project_id: #{resource.id}, token_user: #{resource_bot.name}, token_id: #{access_token.id}")
+ end
end
shared_examples 'rollback revoke steps' do
diff --git a/spec/services/resource_events/change_milestone_service_spec.rb b/spec/services/resource_events/change_milestone_service_spec.rb
index a2131c5c1b0..ed234376381 100644
--- a/spec/services/resource_events/change_milestone_service_spec.rb
+++ b/spec/services/resource_events/change_milestone_service_spec.rb
@@ -6,8 +6,8 @@ RSpec.describe ResourceEvents::ChangeMilestoneService do
let_it_be(:timebox) { create(:milestone) }
let(:created_at_time) { Time.utc(2019, 12, 30) }
- let(:add_timebox_args) { { created_at: created_at_time, old_milestone: nil } }
- let(:remove_timebox_args) { { created_at: created_at_time, old_milestone: timebox } }
+ let(:add_timebox_args) { { old_milestone: nil } }
+ let(:remove_timebox_args) { { old_milestone: timebox } }
[:issue, :merge_request].each do |issuable|
it_behaves_like 'timebox(milestone or iteration) resource events creator', ResourceMilestoneEvent do
diff --git a/spec/services/search/global_service_spec.rb b/spec/services/search/global_service_spec.rb
index 7b914a4d3d6..e8716ef4d90 100644
--- a/spec/services/search/global_service_spec.rb
+++ b/spec/services/search/global_service_spec.rb
@@ -56,14 +56,20 @@ RSpec.describe Search::GlobalService do
context 'issues' do
let(:scope) { 'issues' }
- context 'sort by created_at' do
- let!(:project) { create(:project, :public) }
+ context 'sorting' do
+ let_it_be(:project) { create(:project, :public) }
+
let!(:old_result) { create(:issue, project: project, title: 'sorted old', created_at: 1.month.ago) }
let!(:new_result) { create(:issue, project: project, title: 'sorted recent', created_at: 1.day.ago) }
let!(:very_old_result) { create(:issue, project: project, title: 'sorted very old', created_at: 1.year.ago) }
+ let!(:old_updated) { create(:issue, project: project, title: 'updated old', updated_at: 1.month.ago) }
+ let!(:new_updated) { create(:issue, project: project, title: 'updated recent', updated_at: 1.day.ago) }
+ let!(:very_old_updated) { create(:issue, project: project, title: 'updated very old', updated_at: 1.year.ago) }
+
include_examples 'search results sorted' do
- let(:results) { described_class.new(nil, search: 'sorted', sort: sort).execute }
+ let(:results_created) { described_class.new(nil, search: 'sorted', sort: sort).execute }
+ let(:results_updated) { described_class.new(nil, search: 'updated', sort: sort).execute }
end
end
end
@@ -71,14 +77,20 @@ RSpec.describe Search::GlobalService do
context 'merge_request' do
let(:scope) { 'merge_requests' }
- context 'sort by created_at' do
+ context 'sorting' do
let!(:project) { create(:project, :public) }
+
let!(:old_result) { create(:merge_request, :opened, source_project: project, source_branch: 'old-1', title: 'sorted old', created_at: 1.month.ago) }
let!(:new_result) { create(:merge_request, :opened, source_project: project, source_branch: 'new-1', title: 'sorted recent', created_at: 1.day.ago) }
let!(:very_old_result) { create(:merge_request, :opened, source_project: project, source_branch: 'very-old-1', title: 'sorted very old', created_at: 1.year.ago) }
+ let!(:old_updated) { create(:merge_request, :opened, source_project: project, source_branch: 'updated-old-1', title: 'updated old', updated_at: 1.month.ago) }
+ let!(:new_updated) { create(:merge_request, :opened, source_project: project, source_branch: 'updated-new-1', title: 'updated recent', updated_at: 1.day.ago) }
+ let!(:very_old_updated) { create(:merge_request, :opened, source_project: project, source_branch: 'updated-very-old-1', title: 'updated very old', updated_at: 1.year.ago) }
+
include_examples 'search results sorted' do
- let(:results) { described_class.new(nil, search: 'sorted', sort: sort).execute }
+ let(:results_created) { described_class.new(nil, search: 'sorted', sort: sort).execute }
+ let(:results_updated) { described_class.new(nil, search: 'updated', sort: sort).execute }
end
end
end
diff --git a/spec/services/search/group_service_spec.rb b/spec/services/search/group_service_spec.rb
index 2bfe714f393..7beeec98b23 100644
--- a/spec/services/search/group_service_spec.rb
+++ b/spec/services/search/group_service_spec.rb
@@ -44,15 +44,21 @@ RSpec.describe Search::GroupService do
context 'issues' do
let(:scope) { 'issues' }
- context 'sort by created_at' do
- let!(:group) { create(:group) }
- let!(:project) { create(:project, :public, group: group) }
+ context 'sorting' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :public, group: group) }
+
let!(:old_result) { create(:issue, project: project, title: 'sorted old', created_at: 1.month.ago) }
let!(:new_result) { create(:issue, project: project, title: 'sorted recent', created_at: 1.day.ago) }
let!(:very_old_result) { create(:issue, project: project, title: 'sorted very old', created_at: 1.year.ago) }
+ let!(:old_updated) { create(:issue, project: project, title: 'updated old', updated_at: 1.month.ago) }
+ let!(:new_updated) { create(:issue, project: project, title: 'updated recent', updated_at: 1.day.ago) }
+ let!(:very_old_updated) { create(:issue, project: project, title: 'updated very old', updated_at: 1.year.ago) }
+
include_examples 'search results sorted' do
- let(:results) { described_class.new(nil, group, search: 'sorted', sort: sort).execute }
+ let(:results_created) { described_class.new(nil, group, search: 'sorted', sort: sort).execute }
+ let(:results_updated) { described_class.new(nil, group, search: 'updated', sort: sort).execute }
end
end
end
@@ -60,15 +66,21 @@ RSpec.describe Search::GroupService do
context 'merge requests' do
let(:scope) { 'merge_requests' }
- context 'sort by created_at' do
+ context 'sorting' do
let!(:group) { create(:group) }
let!(:project) { create(:project, :public, group: group) }
+
let!(:old_result) { create(:merge_request, :opened, source_project: project, source_branch: 'old-1', title: 'sorted old', created_at: 1.month.ago) }
let!(:new_result) { create(:merge_request, :opened, source_project: project, source_branch: 'new-1', title: 'sorted recent', created_at: 1.day.ago) }
let!(:very_old_result) { create(:merge_request, :opened, source_project: project, source_branch: 'very-old-1', title: 'sorted very old', created_at: 1.year.ago) }
+ let!(:old_updated) { create(:merge_request, :opened, source_project: project, source_branch: 'updated-old-1', title: 'updated old', updated_at: 1.month.ago) }
+ let!(:new_updated) { create(:merge_request, :opened, source_project: project, source_branch: 'updated-new-1', title: 'updated recent', updated_at: 1.day.ago) }
+ let!(:very_old_updated) { create(:merge_request, :opened, source_project: project, source_branch: 'updated-very-old-1', title: 'updated very old', updated_at: 1.year.ago) }
+
include_examples 'search results sorted' do
- let(:results) { described_class.new(nil, group, search: 'sorted', sort: sort).execute }
+ let(:results_created) { described_class.new(nil, group, search: 'sorted', sort: sort).execute }
+ let(:results_updated) { described_class.new(nil, group, search: 'updated', sort: sort).execute }
end
end
end
diff --git a/spec/services/security/ci_configuration/sast_create_service_spec.rb b/spec/services/security/ci_configuration/sast_create_service_spec.rb
new file mode 100644
index 00000000000..ff7ab614e08
--- /dev/null
+++ b/spec/services/security/ci_configuration/sast_create_service_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Security::CiConfiguration::SastCreateService, :snowplow do
+ describe '#execute' do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+ let(:params) { {} }
+
+ subject(:result) { described_class.new(project, user, params).execute }
+
+ context 'user does not belong to project' do
+ it 'returns an error status' do
+ expect(result[:status]).to eq(:error)
+ expect(result[:success_path]).to be_nil
+ end
+
+ it 'does not track a snowplow event' do
+ subject
+
+ expect_no_snowplow_event
+ end
+ end
+
+ context 'user belongs to project' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'does track the snowplow event' do
+ subject
+
+ expect_snowplow_event(
+ category: 'Security::CiConfiguration::SastCreateService',
+ action: 'create',
+ label: 'false'
+ )
+ end
+
+ it 'raises exception if the user does not have permission to create a new branch' do
+ allow(project).to receive(:repository).and_raise(Gitlab::Git::PreReceiveError, "You are not allowed to create protected branches on this project.")
+
+ expect { subject }.to raise_error(Gitlab::Git::PreReceiveError)
+ end
+
+ context 'with no parameters' do
+ it 'returns the path to create a new merge request' do
+ expect(result[:status]).to eq(:success)
+ expect(result[:success_path]).to match(/#{Gitlab::Routing.url_helpers.project_new_merge_request_url(project, {})}(.*)description(.*)source_branch/)
+ end
+ end
+
+ context 'with parameters' do
+ let(:params) do
+ { 'stage' => 'security',
+ 'SEARCH_MAX_DEPTH' => 1,
+ 'SECURE_ANALYZERS_PREFIX' => 'new_registry',
+ 'SAST_EXCLUDED_PATHS' => 'spec,docs' }
+ end
+
+ it 'returns the path to create a new merge request' do
+ expect(result[:status]).to eq(:success)
+ expect(result[:success_path]).to match(/#{Gitlab::Routing.url_helpers.project_new_merge_request_url(project, {})}(.*)description(.*)source_branch/)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/security/ci_configuration/sast_parser_service_spec.rb b/spec/services/security/ci_configuration/sast_parser_service_spec.rb
new file mode 100644
index 00000000000..21490f993c7
--- /dev/null
+++ b/spec/services/security/ci_configuration/sast_parser_service_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Security::CiConfiguration::SastParserService do
+ describe '#configuration' do
+ include_context 'read ci configuration for sast enabled project'
+
+ let(:configuration) { described_class.new(project).configuration }
+ let(:secure_analyzers_prefix) { configuration['global'][0] }
+ let(:sast_excluded_paths) { configuration['global'][1] }
+ let(:sast_analyzer_image_tag) { configuration['global'][2] }
+ let(:sast_pipeline_stage) { configuration['pipeline'][0] }
+ let(:sast_search_max_depth) { configuration['pipeline'][1] }
+ let(:brakeman) { configuration['analyzers'][0] }
+ let(:bandit) { configuration['analyzers'][1] }
+ let(:sast_brakeman_level) { brakeman['variables'][0] }
+
+ it 'parses the configuration for SAST' do
+ expect(secure_analyzers_prefix['default_value']).to eql('registry.gitlab.com/gitlab-org/security-products/analyzers')
+ expect(sast_excluded_paths['default_value']).to eql('spec, test, tests, tmp')
+ expect(sast_analyzer_image_tag['default_value']).to eql('2')
+ expect(sast_pipeline_stage['default_value']).to eql('test')
+ expect(sast_search_max_depth['default_value']).to eql('4')
+ expect(brakeman['enabled']).to be(true)
+ expect(sast_brakeman_level['default_value']).to eql('1')
+ end
+
+ context 'while populating current values of the entities' do
+ context 'when .gitlab-ci.yml is present' do
+ it 'populates the current values from the file' do
+ allow(project.repository).to receive(:blob_data_at).and_return(gitlab_ci_yml_content)
+ expect(secure_analyzers_prefix['value']).to eql('registry.gitlab.com/gitlab-org/security-products/analyzers2')
+ expect(sast_excluded_paths['value']).to eql('spec, executables')
+ expect(sast_analyzer_image_tag['value']).to eql('2')
+ expect(sast_pipeline_stage['value']).to eql('our_custom_security_stage')
+ expect(sast_search_max_depth['value']).to eql('8')
+ expect(brakeman['enabled']).to be(false)
+ expect(bandit['enabled']).to be(true)
+ expect(sast_brakeman_level['value']).to eql('2')
+ end
+
+ context 'SAST_DEFAULT_ANALYZERS is set' do
+ it 'enables analyzers correctly' do
+ allow(project.repository).to receive(:blob_data_at).and_return(gitlab_ci_yml_default_analyzers_content)
+
+ expect(brakeman['enabled']).to be(false)
+ expect(bandit['enabled']).to be(true)
+ end
+ end
+
+ context 'SAST_EXCLUDED_ANALYZERS is set' do
+ it 'enables analyzers correctly' do
+ allow(project.repository).to receive(:blob_data_at).and_return(gitlab_ci_yml_excluded_analyzers_content)
+
+ expect(brakeman['enabled']).to be(false)
+ expect(bandit['enabled']).to be(true)
+ end
+ end
+ end
+
+ context 'when .gitlab-ci.yml is absent' do
+ it 'populates the current values with the default values' do
+ allow(project.repository).to receive(:blob_data_at).and_return(nil)
+ expect(secure_analyzers_prefix['value']).to eql('registry.gitlab.com/gitlab-org/security-products/analyzers')
+ expect(sast_excluded_paths['value']).to eql('spec, test, tests, tmp')
+ expect(sast_analyzer_image_tag['value']).to eql('2')
+ expect(sast_pipeline_stage['value']).to eql('test')
+ expect(sast_search_max_depth['value']).to eql('4')
+ expect(brakeman['enabled']).to be(true)
+ expect(sast_brakeman_level['value']).to eql('1')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/snippets/create_service_spec.rb b/spec/services/snippets/create_service_spec.rb
index 96807fd629f..32a09e1afc8 100644
--- a/spec/services/snippets/create_service_spec.rb
+++ b/spec/services/snippets/create_service_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe Snippets::CreateService do
describe '#execute' do
let_it_be(:user) { create(:user) }
let_it_be(:admin) { create(:user, :admin) }
+ let(:action) { :create }
let(:opts) { base_opts.merge(extra_opts) }
let(:base_opts) do
{
@@ -309,7 +310,7 @@ RSpec.describe Snippets::CreateService do
it_behaves_like 'a service that creates a snippet'
it_behaves_like 'public visibility level restrictions apply'
- it_behaves_like 'snippets spam check is performed'
+ it_behaves_like 'checking spam'
it_behaves_like 'snippet create data is tracked'
it_behaves_like 'an error service response when save fails'
it_behaves_like 'creates repository and files'
@@ -337,7 +338,7 @@ RSpec.describe Snippets::CreateService do
it_behaves_like 'a service that creates a snippet'
it_behaves_like 'public visibility level restrictions apply'
- it_behaves_like 'snippets spam check is performed'
+ it_behaves_like 'checking spam'
it_behaves_like 'snippet create data is tracked'
it_behaves_like 'an error service response when save fails'
it_behaves_like 'creates repository and files'
diff --git a/spec/services/snippets/update_repository_storage_service_spec.rb b/spec/services/snippets/update_repository_storage_service_spec.rb
index b2bcd620d76..6ba09a9dca9 100644
--- a/spec/services/snippets/update_repository_storage_service_spec.rb
+++ b/spec/services/snippets/update_repository_storage_service_spec.rb
@@ -54,13 +54,18 @@ RSpec.describe Snippets::UpdateRepositoryStorageService do
end
context 'when the filesystems are the same' do
- let(:destination) { snippet.repository_storage }
+ before do
+ expect(Gitlab::GitalyClient).to receive(:filesystem_id).twice.and_return(SecureRandom.uuid)
+ end
- it 'bails out and does nothing' do
+ it 'updates the database without trying to move the repostory', :aggregate_failures do
result = subject.execute
+ snippet.reload
- expect(result).to be_error
- expect(result.message).to match(/SameFilesystemError/)
+ expect(result).to be_success
+ expect(snippet).not_to be_repository_read_only
+ expect(snippet.repository_storage).to eq(destination)
+ expect(snippet.snippet_repository.shard_name).to eq(destination)
end
end
diff --git a/spec/services/snippets/update_service_spec.rb b/spec/services/snippets/update_service_spec.rb
index a2341dc71b2..e737c00ae67 100644
--- a/spec/services/snippets/update_service_spec.rb
+++ b/spec/services/snippets/update_service_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe Snippets::UpdateService do
describe '#execute', :aggregate_failures do
let_it_be(:user) { create(:user) }
let_it_be(:admin) { create :user, admin: true }
+ let(:action) { :update }
let(:visibility_level) { Gitlab::VisibilityLevel::PRIVATE }
let(:base_opts) do
{
@@ -738,11 +739,7 @@ RSpec.describe Snippets::UpdateService do
it_behaves_like 'only file_name is present'
it_behaves_like 'only content is present'
it_behaves_like 'invalid params error response'
- it_behaves_like 'snippets spam check is performed' do
- before do
- subject
- end
- end
+ it_behaves_like 'checking spam'
context 'when snippet does not have a repository' do
let!(:snippet) { create(:project_snippet, author: user, project: project) }
@@ -766,11 +763,7 @@ RSpec.describe Snippets::UpdateService do
it_behaves_like 'only file_name is present'
it_behaves_like 'only content is present'
it_behaves_like 'invalid params error response'
- it_behaves_like 'snippets spam check is performed' do
- before do
- subject
- end
- end
+ it_behaves_like 'checking spam'
context 'when snippet does not have a repository' do
let!(:snippet) { create(:personal_snippet, author: user, project: project) }
diff --git a/spec/services/spam/spam_action_service_spec.rb b/spec/services/spam/spam_action_service_spec.rb
index 8edd9406bce..371923f1518 100644
--- a/spec/services/spam/spam_action_service_spec.rb
+++ b/spec/services/spam/spam_action_service_spec.rb
@@ -24,41 +24,16 @@ RSpec.describe Spam::SpamActionService do
issue.spam = false
end
- describe '#initialize' do
- subject { described_class.new(spammable: issue, request: request, user: user) }
-
- context 'when the request is nil' do
- let(:request) { nil }
-
- it 'assembles the options with information from the spammable' do
- aggregate_failures do
- expect(subject.options[:ip_address]).to eq(issue.ip_address)
- expect(subject.options[:user_agent]).to eq(issue.user_agent)
- expect(subject.options.key?(:referrer)).to be_falsey
- end
- end
- end
-
- context 'when the request is present' do
- let(:request) { double(:request, env: env) }
-
- it 'assembles the options with information from the spammable' do
- aggregate_failures do
- expect(subject.options[:ip_address]).to eq(fake_ip)
- expect(subject.options[:user_agent]).to eq(fake_user_agent)
- expect(subject.options[:referrer]).to eq(fake_referrer)
- end
- end
- end
- end
-
shared_examples 'only checks for spam if a request is provided' do
context 'when request is missing' do
- subject { described_class.new(spammable: issue, request: nil, user: user) }
+ let(:request) { nil }
it "doesn't check as spam" do
- subject
+ expect(fake_verdict_service).not_to receive(:execute)
+
+ response = subject
+ expect(response.message).to match(/request was not present/)
expect(issue).not_to be_spam
end
end
@@ -66,34 +41,88 @@ RSpec.describe Spam::SpamActionService do
context 'when request exists' do
it 'creates a spam log' do
expect { subject }
- .to log_spam(title: issue.title, description: issue.description, noteable_type: 'Issue')
+ .to log_spam(title: issue.title, description: issue.description, noteable_type: 'Issue')
end
end
end
+ shared_examples 'creates a spam log' do
+ it do
+ expect { subject }.to change { SpamLog.count }.by(1)
+
+ new_spam_log = SpamLog.last
+ expect(new_spam_log.user_id).to eq(user.id)
+ expect(new_spam_log.title).to eq(issue.title)
+ expect(new_spam_log.description).to eq(issue.description)
+ expect(new_spam_log.source_ip).to eq(fake_ip)
+ expect(new_spam_log.user_agent).to eq(fake_user_agent)
+ expect(new_spam_log.noteable_type).to eq('Issue')
+ expect(new_spam_log.via_api).to eq(false)
+ end
+ end
+
describe '#execute' do
let(:request) { double(:request, env: env) }
+ let(:fake_captcha_verification_service) { double(:captcha_verification_service) }
let(:fake_verdict_service) { double(:spam_verdict_service) }
let(:allowlisted) { false }
+ let(:api) { nil }
+ let(:captcha_response) { 'abc123' }
+ let(:spam_log_id) { existing_spam_log.id }
+ let(:spam_params) do
+ Spam::SpamActionService.filter_spam_params!(
+ api: api,
+ captcha_response: captcha_response,
+ spam_log_id: spam_log_id
+ )
+ end
+
+ let(:verdict_service_opts) do
+ {
+ ip_address: fake_ip,
+ user_agent: fake_user_agent,
+ referrer: fake_referrer
+ }
+ end
+
+ let(:verdict_service_args) do
+ {
+ target: issue,
+ user: user,
+ request: request,
+ options: verdict_service_opts,
+ context: {
+ action: :create,
+ target_type: 'Issue'
+ }
+ }
+ end
let_it_be(:existing_spam_log) { create(:spam_log, user: user, recaptcha_verified: false) }
subject do
- described_service = described_class.new(spammable: issue, request: request, user: user)
+ described_service = described_class.new(spammable: issue, request: request, user: user, action: :create)
allow(described_service).to receive(:allowlisted?).and_return(allowlisted)
- described_service.execute(api: nil, recaptcha_verified: recaptcha_verified, spam_log_id: existing_spam_log.id)
+ described_service.execute(spam_params: spam_params)
end
before do
- allow(Spam::SpamVerdictService).to receive(:new).and_return(fake_verdict_service)
+ allow(Captcha::CaptchaVerificationService).to receive(:new) { fake_captcha_verification_service }
+ allow(Spam::SpamVerdictService).to receive(:new).with(verdict_service_args).and_return(fake_verdict_service)
end
- context 'when reCAPTCHA was already verified' do
- let(:recaptcha_verified) { true }
+ context 'when captcha response verification returns true' do
+ before do
+ expect(fake_captcha_verification_service)
+ .to receive(:execute).with(captcha_response: captcha_response, request: request) { true }
+ end
it "doesn't check with the SpamVerdictService" do
aggregate_failures do
- expect(SpamLog).to receive(:verify_recaptcha!)
+ expect(SpamLog).to receive(:verify_recaptcha!).with(
+ user_id: user.id,
+ id: spam_log_id
+ )
expect(fake_verdict_service).not_to receive(:execute)
end
@@ -105,8 +134,11 @@ RSpec.describe Spam::SpamActionService do
end
end
- context 'when reCAPTCHA was not verified' do
- let(:recaptcha_verified) { false }
+ context 'when captcha response verification returns false' do
+ before do
+ expect(fake_captcha_verification_service)
+ .to receive(:execute).with(captcha_response: captcha_response, request: request) { false }
+ end
context 'when spammable attributes have not changed' do
before do
@@ -120,6 +152,10 @@ RSpec.describe Spam::SpamActionService do
end
context 'when spammable attributes have changed' do
+ let(:expected_service_check_response_message) do
+ /check Issue spammable model for any errors or captcha requirement/
+ end
+
before do
issue.description = 'SPAM!'
end
@@ -130,7 +166,9 @@ RSpec.describe Spam::SpamActionService do
it 'does not perform spam check' do
expect(Spam::SpamVerdictService).not_to receive(:new)
- subject
+ response = subject
+
+ expect(response.message).to match(/user was allowlisted/)
end
end
@@ -147,8 +185,9 @@ RSpec.describe Spam::SpamActionService do
it_behaves_like 'only checks for spam if a request is provided'
it 'marks as spam' do
- subject
+ response = subject
+ expect(response.message).to match(expected_service_check_response_message)
expect(issue).to be_spam
end
end
@@ -157,8 +196,9 @@ RSpec.describe Spam::SpamActionService do
it_behaves_like 'only checks for spam if a request is provided'
it 'does not mark as spam' do
- subject
+ response = subject
+ expect(response.message).to match(expected_service_check_response_message)
expect(issue).not_to be_spam
end
end
@@ -176,15 +216,19 @@ RSpec.describe Spam::SpamActionService do
it_behaves_like 'only checks for spam if a request is provided'
+ it_behaves_like 'creates a spam log'
+
it 'does not mark as spam' do
- subject
+ response = subject
+ expect(response.message).to match(expected_service_check_response_message)
expect(issue).not_to be_spam
end
it 'marks as needing reCAPTCHA' do
- subject
+ response = subject
+ expect(response.message).to match(expected_service_check_response_message)
expect(issue.needs_recaptcha?).to be_truthy
end
end
@@ -192,9 +236,12 @@ RSpec.describe Spam::SpamActionService do
context 'when allow_possible_spam feature flag is true' do
it_behaves_like 'only checks for spam if a request is provided'
+ it_behaves_like 'creates a spam log'
+
it 'does not mark as needing reCAPTCHA' do
- subject
+ response = subject
+ expect(response.message).to match(expected_service_check_response_message)
expect(issue.needs_recaptcha).to be_falsey
end
end
@@ -209,6 +256,51 @@ RSpec.describe Spam::SpamActionService do
expect { subject }
.not_to change { SpamLog.count }
end
+
+ it 'clears spam flags' do
+ expect(issue).to receive(:clear_spam_flags!)
+
+ subject
+ end
+ end
+
+ context 'spam verdict service options' do
+ before do
+ allow(fake_verdict_service).to receive(:execute) { ALLOW }
+ end
+
+ context 'when the request is nil' do
+ let(:request) { nil }
+ let(:issue_ip_address) { '1.2.3.4' }
+ let(:issue_user_agent) { 'lynx' }
+ let(:verdict_service_opts) do
+ {
+ ip_address: issue_ip_address,
+ user_agent: issue_user_agent
+ }
+ end
+
+ before do
+ allow(issue).to receive(:ip_address) { issue_ip_address }
+ allow(issue).to receive(:user_agent) { issue_user_agent }
+ end
+
+ it 'assembles the options with information from the spammable' do
+ # TODO: This code untestable, because we do not perform a verification if there is not a
+ # request. See corresponding comment in code
+ # expect(Spam::SpamVerdictService).to receive(:new).with(verdict_service_args)
+
+ subject
+ end
+ end
+
+ context 'when the request is present' do
+ it 'assembles the options with information from the request' do
+ expect(Spam::SpamVerdictService).to receive(:new).with(verdict_service_args)
+
+ subject
+ end
+ end
end
end
end
diff --git a/spec/services/suggestions/apply_service_spec.rb b/spec/services/suggestions/apply_service_spec.rb
index 3e7594bd30f..77d0e892725 100644
--- a/spec/services/suggestions/apply_service_spec.rb
+++ b/spec/services/suggestions/apply_service_spec.rb
@@ -32,8 +32,8 @@ RSpec.describe Suggestions::ApplyService do
create(:suggestion, :content_from_repo, suggestion_args)
end
- def apply(suggestions)
- result = apply_service.new(user, *suggestions).execute
+ def apply(suggestions, custom_message = nil)
+ result = apply_service.new(user, *suggestions, message: custom_message).execute
suggestions.map { |suggestion| suggestion.reload }
@@ -74,6 +74,14 @@ RSpec.describe Suggestions::ApplyService do
expect(commit.author_name).to eq(user.name)
end
+ it 'tracks apply suggestion event' do
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to receive(:track_apply_suggestion_action)
+ .with(user: user)
+
+ apply(suggestions)
+ end
+
context 'when a custom suggestion commit message' do
before do
project.update!(suggestion_commit_message: message)
@@ -103,6 +111,16 @@ RSpec.describe Suggestions::ApplyService do
end
end
end
+
+ context 'with a user suggested commit message' do
+ let(:message) { "i'm a custom commit message!" }
+
+ it "uses the user's commit message" do
+ apply(suggestions, message)
+
+ expect(project.repository.commit.message).to(eq(message))
+ end
+ end
end
subject(:apply_service) { described_class }
@@ -570,56 +588,84 @@ RSpec.describe Suggestions::ApplyService do
project.add_maintainer(user)
end
+ shared_examples_for 'service not tracking apply suggestion event' do
+ it 'does not track apply suggestion event' do
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .not_to receive(:track_apply_suggestion_action)
+
+ result
+ end
+ end
+
context 'diff file was not found' do
- it 'returns error message' do
- expect(suggestion.note).to receive(:latest_diff_file) { nil }
+ let(:result) { apply_service.new(user, suggestion).execute }
- result = apply_service.new(user, suggestion).execute
+ before do
+ expect(suggestion.note).to receive(:latest_diff_file) { nil }
+ end
+ it 'returns error message' do
expect(result).to eq(message: 'A file was not found.',
status: :error)
end
+
+ it_behaves_like 'service not tracking apply suggestion event'
end
context 'when not all suggestions belong to the same branch' do
- it 'renders error message' do
- merge_request2 = create(:merge_request,
- :conflict,
- source_project: project,
- target_project: project)
-
- position2 = Gitlab::Diff::Position.new(old_path: "files/ruby/popen.rb",
- new_path: "files/ruby/popen.rb",
- old_line: nil,
- new_line: 15,
- diff_refs: merge_request2
- .diff_refs)
+ let(:merge_request2) do
+ create(
+ :merge_request,
+ :conflict,
+ source_project: project,
+ target_project: project
+ )
+ end
- diff_note2 = create(:diff_note_on_merge_request,
- noteable: merge_request2,
- position: position2,
- project: project)
+ let(:position2) do
+ Gitlab::Diff::Position.new(
+ old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: nil,
+ new_line: 15,
+ diff_refs: merge_request2.diff_refs
+ )
+ end
- other_branch_suggestion = create(:suggestion, note: diff_note2)
+ let(:diff_note2) do
+ create(
+ :diff_note_on_merge_request,
+ noteable: merge_request2,
+ position: position2,
+ project: project
+ )
+ end
- result = apply_service.new(user, suggestion, other_branch_suggestion).execute
+ let(:other_branch_suggestion) { create(:suggestion, note: diff_note2) }
+ let(:result) { apply_service.new(user, suggestion, other_branch_suggestion).execute }
+ it 'renders error message' do
expect(result).to eq(message: 'Suggestions must all be on the same branch.',
status: :error)
end
+
+ it_behaves_like 'service not tracking apply suggestion event'
end
context 'suggestion is not appliable' do
let(:inapplicable_reason) { "Can't apply this suggestion." }
+ let(:result) { apply_service.new(user, suggestion).execute }
- it 'returns error message' do
+ before do
expect(suggestion).to receive(:appliable?).and_return(false)
expect(suggestion).to receive(:inapplicable_reason).and_return(inapplicable_reason)
+ end
- result = apply_service.new(user, suggestion).execute
-
+ it 'returns error message' do
expect(result).to eq(message: inapplicable_reason, status: :error)
end
+
+ it_behaves_like 'service not tracking apply suggestion event'
end
context 'lines of suggestions overlap' do
@@ -632,12 +678,14 @@ RSpec.describe Suggestions::ApplyService do
create_suggestion(to_content: "I Overlap!")
end
- it 'returns error message' do
- result = apply_service.new(user, suggestion, overlapping_suggestion).execute
+ let(:result) { apply_service.new(user, suggestion, overlapping_suggestion).execute }
+ it 'returns error message' do
expect(result).to eq(message: 'Suggestions are not applicable as their lines cannot overlap.',
status: :error)
end
+
+ it_behaves_like 'service not tracking apply suggestion event'
end
end
end
diff --git a/spec/services/suggestions/create_service_spec.rb b/spec/services/suggestions/create_service_spec.rb
index 80823364fe8..5148d6756fc 100644
--- a/spec/services/suggestions/create_service_spec.rb
+++ b/spec/services/suggestions/create_service_spec.rb
@@ -53,6 +53,15 @@ RSpec.describe Suggestions::CreateService do
subject { described_class.new(note) }
+ shared_examples_for 'service not tracking add suggestion event' do
+ it 'does not track add suggestion event' do
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .not_to receive(:track_add_suggestion_action)
+
+ subject.execute
+ end
+ end
+
describe '#execute' do
context 'should not try to parse suggestions' do
context 'when not a diff note for merge requests' do
@@ -66,6 +75,8 @@ RSpec.describe Suggestions::CreateService do
subject.execute
end
+
+ it_behaves_like 'service not tracking add suggestion event'
end
context 'when diff note is not for text' do
@@ -76,17 +87,21 @@ RSpec.describe Suggestions::CreateService do
note: markdown)
end
- it 'does not try to parse suggestions' do
+ before do
allow(note).to receive(:on_text?) { false }
+ end
+ it 'does not try to parse suggestions' do
expect(Gitlab::Diff::SuggestionsParser).not_to receive(:parse)
subject.execute
end
+
+ it_behaves_like 'service not tracking add suggestion event'
end
end
- context 'should not create suggestions' do
+ context 'when diff file is not found' do
let(:note) do
create(:diff_note_on_merge_request, project: project_with_repo,
noteable: merge_request,
@@ -94,13 +109,17 @@ RSpec.describe Suggestions::CreateService do
note: markdown)
end
- it 'creates no suggestion when diff file is not found' do
+ before do
expect_next_instance_of(DiffNote) do |diff_note|
expect(diff_note).to receive(:latest_diff_file).once { nil }
end
+ end
+ it 'creates no suggestion' do
expect { subject.execute }.not_to change(Suggestion, :count)
end
+
+ it_behaves_like 'service not tracking add suggestion event'
end
context 'should create suggestions' do
@@ -137,6 +156,14 @@ RSpec.describe Suggestions::CreateService do
end
end
+ it 'tracks add suggestion event' do
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to receive(:track_add_suggestion_action)
+ .with(user: note.author)
+
+ subject.execute
+ end
+
context 'outdated position note' do
let!(:outdated_diff) { merge_request.merge_request_diff }
let!(:latest_diff) { merge_request.create_merge_request_diff }
diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb
index 2020c67f465..1ec5237370f 100644
--- a/spec/services/system_hooks_service_spec.rb
+++ b/spec/services/system_hooks_service_spec.rb
@@ -45,15 +45,13 @@ RSpec.describe SystemHooksService do
it do
expect(event_data(group, :create)).to include(
- :event_name, :name, :created_at, :updated_at, :path, :group_id,
- :owner_name, :owner_email
+ :event_name, :name, :created_at, :updated_at, :path, :group_id
)
end
it do
expect(event_data(group, :destroy)).to include(
- :event_name, :name, :created_at, :updated_at, :path, :group_id,
- :owner_name, :owner_email
+ :event_name, :name, :created_at, :updated_at, :path, :group_id
)
end
@@ -156,9 +154,6 @@ RSpec.describe SystemHooksService do
it { expect(event_name(project_member, :update)).to eq "user_update_for_team" }
it { expect(event_name(key, :create)).to eq 'key_create' }
it { expect(event_name(key, :destroy)).to eq 'key_destroy' }
- it { expect(event_name(group, :create)).to eq 'group_create' }
- it { expect(event_name(group, :destroy)).to eq 'group_destroy' }
- it { expect(event_name(group, :rename)).to eq 'group_rename' }
end
def event_data(*args)
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 9c35f9e3817..df4880dfa13 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -213,15 +213,16 @@ RSpec.describe SystemNoteService do
describe '.change_branch' do
it 'calls MergeRequestsService' do
- old_branch = double
- new_branch = double
- branch_type = double
+ old_branch = double('old_branch')
+ new_branch = double('new_branch')
+ branch_type = double('branch_type')
+ event_type = double('event_type')
expect_next_instance_of(::SystemNotes::MergeRequestsService) do |service|
- expect(service).to receive(:change_branch).with(branch_type, old_branch, new_branch)
+ expect(service).to receive(:change_branch).with(branch_type, event_type, old_branch, new_branch)
end
- described_class.change_branch(noteable, project, author, branch_type, old_branch, new_branch)
+ described_class.change_branch(noteable, project, author, branch_type, event_type, old_branch, new_branch)
end
end
diff --git a/spec/services/system_notes/issuables_service_spec.rb b/spec/services/system_notes/issuables_service_spec.rb
index 96afca2f2cb..ae18bc23c17 100644
--- a/spec/services/system_notes/issuables_service_spec.rb
+++ b/spec/services/system_notes/issuables_service_spec.rb
@@ -729,12 +729,14 @@ RSpec.describe ::SystemNotes::IssuablesService do
it 'is false with issue tracker supporting referencing' do
create(:jira_service, project: project)
+ project.reload
expect(service.cross_reference_disallowed?(noteable)).to be_falsey
end
it 'is true with issue tracker not supporting referencing' do
create(:bugzilla_service, project: project)
+ project.reload
expect(service.cross_reference_disallowed?(noteable)).to be_truthy
end
diff --git a/spec/services/system_notes/merge_requests_service_spec.rb b/spec/services/system_notes/merge_requests_service_spec.rb
index 50d16231e8f..2131f3d3bdf 100644
--- a/spec/services/system_notes/merge_requests_service_spec.rb
+++ b/spec/services/system_notes/merge_requests_service_spec.rb
@@ -167,18 +167,38 @@ RSpec.describe ::SystemNotes::MergeRequestsService do
end
describe '.change_branch' do
- subject { service.change_branch('target', old_branch, new_branch) }
-
let(:old_branch) { 'old_branch'}
let(:new_branch) { 'new_branch'}
it_behaves_like 'a system note' do
let(:action) { 'branch' }
+
+ subject { service.change_branch('target', 'update', old_branch, new_branch) }
end
context 'when target branch name changed' do
- it 'sets the note text' do
- expect(subject.note).to eq "changed target branch from `#{old_branch}` to `#{new_branch}`"
+ context 'on update' do
+ subject { service.change_branch('target', 'update', old_branch, new_branch) }
+
+ it 'sets the note text' do
+ expect(subject.note).to eq "changed target branch from `#{old_branch}` to `#{new_branch}`"
+ end
+ end
+
+ context 'on delete' do
+ subject { service.change_branch('target', 'delete', old_branch, new_branch) }
+
+ it 'sets the note text' do
+ expect(subject.note).to eq "changed automatically target branch to `#{new_branch}` because `#{old_branch}` was deleted"
+ end
+ end
+
+ context 'for invalid event_type' do
+ subject { service.change_branch('target', 'invalid', old_branch, new_branch) }
+
+ it 'raises exception' do
+ expect { subject }.to raise_error /invalid value for event_type/
+ end
end
end
end
diff --git a/spec/services/test_hooks/project_service_spec.rb b/spec/services/test_hooks/project_service_spec.rb
index 7470bdff527..a87e612e378 100644
--- a/spec/services/test_hooks/project_service_spec.rb
+++ b/spec/services/test_hooks/project_service_spec.rb
@@ -3,11 +3,13 @@
require 'spec_helper'
RSpec.describe TestHooks::ProjectService do
+ include AfterNextHelpers
+
let(:current_user) { create(:user) }
describe '#execute' do
- let(:project) { create(:project, :repository) }
- let(:hook) { create(:project_hook, project: project) }
+ let_it_be(:project) { create(:project, :repository) }
+ let(:hook) { create(:project_hook, project: project) }
let(:trigger) { 'not_implemented_events' }
let(:service) { described_class.new(hook, current_user, trigger) }
let(:sample_data) { { data: 'sample' } }
@@ -61,17 +63,17 @@ RSpec.describe TestHooks::ProjectService do
end
it 'executes hook' do
- allow(project).to receive(:notes).and_return([Note.new])
+ create(:note, project: project)
+
allow(Gitlab::DataBuilder::Note).to receive(:build).and_return(sample_data)
+ allow_next(NotesFinder).to receive(:execute).and_return(Note.all)
expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
- context 'issues_events' do
- let(:trigger) { 'issues_events' }
- let(:trigger_key) { :issue_hooks }
+ shared_examples_for 'a test webhook that operates on issues' do
let(:issue) { build(:issue) }
it 'returns error message if not enough data' do
@@ -80,36 +82,32 @@ RSpec.describe TestHooks::ProjectService do
end
it 'executes hook' do
- allow(project).to receive(:issues).and_return([issue])
allow(issue).to receive(:to_hook_data).and_return(sample_data)
+ allow_next(IssuesFinder).to receive(:execute).and_return([issue])
expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
+ context 'issues_events' do
+ let(:trigger) { 'issues_events' }
+ let(:trigger_key) { :issue_hooks }
+
+ it_behaves_like 'a test webhook that operates on issues'
+ end
+
context 'confidential_issues_events' do
let(:trigger) { 'confidential_issues_events' }
let(:trigger_key) { :confidential_issue_hooks }
- let(:issue) { build(:issue) }
- it 'returns error message if not enough data' do
- expect(hook).not_to receive(:execute)
- expect(service.execute).to include({ status: :error, message: 'Ensure the project has issues.' })
- end
-
- it 'executes hook' do
- allow(project).to receive(:issues).and_return([issue])
- allow(issue).to receive(:to_hook_data).and_return(sample_data)
-
- expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
- expect(service.execute).to include(success_result)
- end
+ it_behaves_like 'a test webhook that operates on issues'
end
context 'merge_requests_events' do
let(:trigger) { 'merge_requests_events' }
let(:trigger_key) { :merge_request_hooks }
+ let(:merge_request) { build(:merge_request) }
it 'returns error message if not enough data' do
expect(hook).not_to receive(:execute)
@@ -117,8 +115,8 @@ RSpec.describe TestHooks::ProjectService do
end
it 'executes hook' do
- create(:merge_request, source_project: project)
- allow_any_instance_of(MergeRequest).to receive(:to_hook_data).and_return(sample_data)
+ allow(merge_request).to receive(:to_hook_data).and_return(sample_data)
+ allow_next(MergeRequestsFinder).to receive(:execute).and_return([merge_request])
expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
expect(service.execute).to include(success_result)
@@ -128,6 +126,7 @@ RSpec.describe TestHooks::ProjectService do
context 'job_events' do
let(:trigger) { 'job_events' }
let(:trigger_key) { :job_hooks }
+ let(:ci_job) { build(:ci_build) }
it 'returns error message if not enough data' do
expect(hook).not_to receive(:execute)
@@ -135,8 +134,8 @@ RSpec.describe TestHooks::ProjectService do
end
it 'executes hook' do
- create(:ci_build, project: project)
allow(Gitlab::DataBuilder::Build).to receive(:build).and_return(sample_data)
+ allow_next(Ci::JobsFinder).to receive(:execute).and_return([ci_job])
expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
expect(service.execute).to include(success_result)
@@ -146,6 +145,7 @@ RSpec.describe TestHooks::ProjectService do
context 'pipeline_events' do
let(:trigger) { 'pipeline_events' }
let(:trigger_key) { :pipeline_hooks }
+ let(:pipeline) { build(:ci_empty_pipeline) }
it 'returns error message if not enough data' do
expect(hook).not_to receive(:execute)
@@ -153,8 +153,8 @@ RSpec.describe TestHooks::ProjectService do
end
it 'executes hook' do
- create(:ci_empty_pipeline, project: project)
allow(Gitlab::DataBuilder::Pipeline).to receive(:build).and_return(sample_data)
+ allow_next(Ci::PipelinesFinder).to receive(:execute).and_return([pipeline])
expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
expect(service.execute).to include(success_result)
@@ -162,7 +162,7 @@ RSpec.describe TestHooks::ProjectService do
end
context 'wiki_page_events' do
- let(:project) { create(:project, :wiki_repo) }
+ let_it_be(:project) { create(:project, :wiki_repo) }
let(:trigger) { 'wiki_page_events' }
let(:trigger_key) { :wiki_page_hooks }
@@ -190,6 +190,7 @@ RSpec.describe TestHooks::ProjectService do
context 'releases_events' do
let(:trigger) { 'releases_events' }
let(:trigger_key) { :release_hooks }
+ let(:release) { build(:release) }
it 'returns error message if not enough data' do
expect(hook).not_to receive(:execute)
@@ -197,8 +198,8 @@ RSpec.describe TestHooks::ProjectService do
end
it 'executes hook' do
- allow(project).to receive(:releases).and_return([Release.new])
- allow_any_instance_of(Release).to receive(:to_hook_data).and_return(sample_data)
+ allow(release).to receive(:to_hook_data).and_return(sample_data)
+ allow_next(ReleasesFinder).to receive(:execute).and_return([release])
expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
expect(service.execute).to include(success_result)
diff --git a/spec/services/test_hooks/system_service_spec.rb b/spec/services/test_hooks/system_service_spec.rb
index 34dd2173b09..e500a1057ab 100644
--- a/spec/services/test_hooks/system_service_spec.rb
+++ b/spec/services/test_hooks/system_service_spec.rb
@@ -3,12 +3,12 @@
require 'spec_helper'
RSpec.describe TestHooks::SystemService do
- let(:current_user) { create(:user) }
+ include AfterNextHelpers
describe '#execute' do
- let(:project) { create(:project, :repository) }
+ let_it_be(:project) { create(:project, :repository) }
let(:hook) { create(:system_hook) }
- let(:service) { described_class.new(hook, current_user, trigger) }
+ let(:service) { described_class.new(hook, project.owner, trigger) }
let(:success_result) { { status: :success, http_status: 200, message: 'ok' } }
before do
@@ -63,6 +63,9 @@ RSpec.describe TestHooks::SystemService do
context 'merge_requests_events' do
let(:trigger) { 'merge_requests_events' }
+ let(:trigger_key) { :merge_request_hooks }
+ let(:merge_request) { build(:merge_request) }
+ let(:sample_data) { { data: 'sample' } }
it 'returns error message if the user does not have any repository with a merge request' do
expect(hook).not_to receive(:execute)
@@ -70,12 +73,8 @@ RSpec.describe TestHooks::SystemService do
end
it 'executes hook' do
- trigger_key = :merge_request_hooks
- sample_data = { data: 'sample' }
- create(:project_member, user: current_user, project: project)
- create(:merge_request, source_project: project)
- allow_any_instance_of(MergeRequest).to receive(:to_hook_data).and_return(sample_data)
-
+ expect(MergeRequest).to receive(:of_projects).and_return([merge_request])
+ expect(merge_request).to receive(:to_hook_data).and_return(sample_data)
expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
expect(service.execute).to include(success_result)
end
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 83d233a8112..743dc080b06 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -1193,6 +1193,17 @@ RSpec.describe TodoService do
end
end
+ describe '#create_request_review_todo' do
+ let(:target) { create(:merge_request, author: author, source_project: project) }
+ let(:reviewer) { create(:user) }
+
+ it 'creates a todo for reviewer' do
+ service.create_request_review_todo(target, author, reviewer)
+
+ should_create_todo(user: reviewer, target: target, action: Todo::REVIEW_REQUESTED)
+ end
+ end
+
def should_create_todo(attributes = {})
attributes.reverse_merge!(
project: project,
diff --git a/spec/services/users/approve_service_spec.rb b/spec/services/users/approve_service_spec.rb
index 55b2c83f4a8..9999e674c7d 100644
--- a/spec/services/users/approve_service_spec.rb
+++ b/spec/services/users/approve_service_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe Users::ApproveService do
it 'returns error result' do
expect(subject[:status]).to eq(:error)
expect(subject[:message])
- .to match(/The user you are trying to approve is not pending an approval/)
+ .to match(/The user you are trying to approve is not pending approval/)
end
end
@@ -61,6 +61,14 @@ RSpec.describe Users::ApproveService do
expect(user.reload).to be_active
end
+ it 'logs approval in application logs' do
+ allow(Gitlab::AppLogger).to receive(:info)
+
+ 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}")
+ end
+
it 'emails the user on approval' do
expect(DeviseMailer).to receive(:user_admin_approval).with(user).and_call_original
expect { subject }.to have_enqueued_mail(DeviseMailer, :user_admin_approval)
@@ -82,6 +90,20 @@ RSpec.describe Users::ApproveService do
.not_to have_enqueued_mail(DeviseMailer, :confirmation_instructions)
end
end
+
+ context 'audit events' do
+ context 'when not licensed' do
+ before do
+ stub_licensed_features(
+ admin_audit_log: false
+ )
+ end
+
+ it 'does not log any audit event' do
+ expect { subject }.not_to change(AuditEvent, :count)
+ end
+ end
+ end
end
context 'pending invitations' do
diff --git a/spec/services/users/batch_status_cleaner_service_spec.rb b/spec/services/users/batch_status_cleaner_service_spec.rb
new file mode 100644
index 00000000000..46a004542d8
--- /dev/null
+++ b/spec/services/users/batch_status_cleaner_service_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Users::BatchStatusCleanerService do
+ let_it_be(:user_status_1) { create(:user_status, emoji: 'coffee', message: 'msg1', clear_status_at: 1.year.ago) }
+ let_it_be(:user_status_2) { create(:user_status, emoji: 'coffee', message: 'msg1', clear_status_at: 1.year.from_now) }
+ let_it_be(:user_status_3) { create(:user_status, emoji: 'coffee', message: 'msg1', clear_status_at: 2.years.ago) }
+ let_it_be(:user_status_4) { create(:user_status, emoji: 'coffee', message: 'msg1') }
+
+ subject(:result) { described_class.execute }
+
+ it 'cleans up scheduled user statuses' do
+ expect(result[:deleted_rows]).to eq(2)
+
+ deleted_statuses = UserStatus.where(user_id: [user_status_1.user_id, user_status_3.user_id])
+ expect(deleted_statuses).to be_empty
+ end
+
+ it 'does not affect rows with future clear_status_at' do
+ expect { result }.not_to change { user_status_2.reload }
+ end
+
+ it 'does not affect rows without clear_status_at' do
+ expect { result }.not_to change { user_status_4.reload }
+ end
+
+ describe 'batch_size' do
+ it 'clears status in batches' do
+ result = described_class.execute(batch_size: 1)
+
+ expect(result[:deleted_rows]).to eq(1)
+
+ result = described_class.execute(batch_size: 1)
+
+ expect(result[:deleted_rows]).to eq(1)
+
+ result = described_class.execute(batch_size: 1)
+
+ expect(result[:deleted_rows]).to eq(0)
+ end
+ end
+end
diff --git a/spec/services/users/build_service_spec.rb b/spec/services/users/build_service_spec.rb
index 446741221b3..b2a7d349ce6 100644
--- a/spec/services/users/build_service_spec.rb
+++ b/spec/services/users/build_service_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Users::BuildService do
+ using RSpec::Parameterized::TableSyntax
+
describe '#execute' do
let(:params) { build_stubbed(:user).slice(:first_name, :last_name, :username, :email, :password) }
@@ -72,8 +74,6 @@ RSpec.describe Users::BuildService do
end
context 'with "user_default_external" application setting' do
- using RSpec::Parameterized::TableSyntax
-
where(:user_default_external, :external, :email, :user_default_internal_regex, :result) do
true | nil | 'fl@example.com' | nil | true
true | true | 'fl@example.com' | nil | true
@@ -192,8 +192,6 @@ RSpec.describe Users::BuildService do
end
context 'with "user_default_external" application setting' do
- using RSpec::Parameterized::TableSyntax
-
where(:user_default_external, :external, :email, :user_default_internal_regex, :result) do
true | nil | 'fl@example.com' | nil | true
true | true | 'fl@example.com' | nil | true
diff --git a/spec/services/users/refresh_authorized_projects_service_spec.rb b/spec/services/users/refresh_authorized_projects_service_spec.rb
index 9404668e3c5..cc01b22f9d2 100644
--- a/spec/services/users/refresh_authorized_projects_service_spec.rb
+++ b/spec/services/users/refresh_authorized_projects_service_spec.rb
@@ -143,6 +143,21 @@ RSpec.describe Users::RefreshAuthorizedProjectsService do
expect(authorizations[0].project_id).to eq(project.id)
expect(authorizations[0].access_level).to eq(Gitlab::Access::MAINTAINER)
end
+
+ it 'logs the details of the refresh' do
+ source = :foo
+ service = described_class.new(user, source: source)
+ user.project_authorizations.delete_all
+
+ expect(Gitlab::AppJsonLogger).to(
+ receive(:info)
+ .with(event: 'authorized_projects_refresh',
+ 'authorized_projects_refresh.source': source,
+ 'authorized_projects_refresh.rows_deleted': 0,
+ 'authorized_projects_refresh.rows_added': 1))
+
+ service.update_authorizations([], [[user.id, project.id, Gitlab::Access::MAINTAINER]])
+ end
end
describe '#fresh_access_levels_per_project' do
diff --git a/spec/services/users/reject_service_spec.rb b/spec/services/users/reject_service_spec.rb
index 07863d1a290..b9aaff5cde5 100644
--- a/spec/services/users/reject_service_spec.rb
+++ b/spec/services/users/reject_service_spec.rb
@@ -48,6 +48,26 @@ RSpec.describe Users::RejectService do
subject
end
+
+ it 'logs rejection in application logs' do
+ allow(Gitlab::AppLogger).to receive(:info)
+
+ 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}")
+ end
+ end
+ end
+
+ context 'audit events' do
+ context 'when not licensed' do
+ before do
+ stub_licensed_features(admin_audit_log: false)
+ end
+
+ it 'does not log any audit event' do
+ expect { subject }.not_to change(AuditEvent, :count)
+ end
end
end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 9fac6c8e192..64c1479a412 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -13,6 +13,7 @@ end
Warning[:deprecated] = true unless ENV.key?('SILENCE_DEPRECATIONS')
require './spec/deprecation_toolkit_env'
+DeprecationToolkitEnv.configure!
require './spec/simplecov_env'
SimpleCovEnv.start!
@@ -33,6 +34,7 @@ require 'rspec-parameterized'
require 'shoulda/matchers'
require 'test_prof/recipes/rspec/let_it_be'
require 'test_prof/factory_default'
+require 'parslet/rig/rspec'
rspec_profiling_is_configured =
ENV['RSPEC_PROFILING_POSTGRES_URL'].present? ||
@@ -174,6 +176,7 @@ RSpec.configure do |config|
if ENV['CI'] || ENV['RETRIES']
# This includes the first try, i.e. tests will be run 4 times before failing.
config.default_retry_count = ENV.fetch('RETRIES', 3).to_i + 1
+ config.exceptions_to_hard_fail = [DeprecationToolkitEnv::DeprecationBehaviors::SelectiveRaise::RaiseDisallowedDeprecation]
end
if ENV['FLAKY_RSPEC_GENERATE_REPORT']
@@ -216,16 +219,9 @@ RSpec.configure do |config|
# (ie. ApplicationSetting#auto_devops_enabled)
stub_feature_flags(force_autodevops_on_by_default: false)
- # The following can be removed once Vue Issuable Sidebar
- # is feature-complete and can be made default in place
- # of older sidebar.
- # See https://gitlab.com/groups/gitlab-org/-/epics/1863
- stub_feature_flags(vue_issuable_sidebar: false)
- stub_feature_flags(vue_issuable_epic_sidebar: 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)
+ # 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
@@ -289,6 +285,8 @@ RSpec.configure do |config|
current_user_mode.send(:user)&.admin?
end
end
+
+ allow(Gitlab::CurrentSettings).to receive(:current_application_settings?).and_return(false)
end
config.around(:example, :quarantine) do |example|
diff --git a/spec/support/factory_bot.rb b/spec/support/factory_bot.rb
index c9d372993b5..5761e05d541 100644
--- a/spec/support/factory_bot.rb
+++ b/spec/support/factory_bot.rb
@@ -3,3 +3,16 @@
FactoryBot::SyntaxRunner.class_eval do
include RSpec::Mocks::ExampleMethods
end
+
+# Patching FactoryBot to allow stubbing non AR models
+# See https://github.com/thoughtbot/factory_bot/pull/1466
+module Gitlab
+ module FactoryBotStubPatch
+ def has_settable_id?(result_instance)
+ result_instance.class.respond_to?(:primary_key) &&
+ result_instance.class.primary_key
+ end
+ end
+end
+
+FactoryBot::Strategy::Stub.prepend(Gitlab::FactoryBotStubPatch)
diff --git a/spec/support/gitlab_experiment.rb b/spec/support/gitlab_experiment.rb
index 1f283e4f06c..45ae9958c52 100644
--- a/spec/support/gitlab_experiment.rb
+++ b/spec/support/gitlab_experiment.rb
@@ -1,4 +1,16 @@
# frozen_string_literal: true
+# Require the provided spec helper and matchers.
+require 'gitlab/experiment/rspec'
+
+# This is a temporary fix until we have a larger discussion around the
+# challenges raised in https://gitlab.com/gitlab-org/gitlab/-/issues/300104
+class ApplicationExperiment < Gitlab::Experiment # rubocop:disable Gitlab/NamespacedClass
+ def initialize(...)
+ super(...)
+ Feature.persist_used!(feature_flag_name)
+ end
+end
+
# Disable all caching for experiments in tests.
Gitlab::Experiment::Configuration.cache = nil
diff --git a/spec/support/graphql/field_selection.rb b/spec/support/graphql/field_selection.rb
index e2a3334acac..00323c46d69 100644
--- a/spec/support/graphql/field_selection.rb
+++ b/spec/support/graphql/field_selection.rb
@@ -47,7 +47,7 @@ module Graphql
NO_SKIP = ->(_name, _field) { false }
def self.select_fields(type, skip = NO_SKIP, parent_types = Set.new, max_depth = 3)
- return new if max_depth <= 0
+ return new if max_depth <= 0 || !type.kind.fields?
new(type.fields.flat_map do |name, field|
next [] if skip[name, field]
diff --git a/spec/support/helpers/cycle_analytics_helpers.rb b/spec/support/helpers/cycle_analytics_helpers.rb
index 6d3ac699a7c..a90cbbf3bd3 100644
--- a/spec/support/helpers/cycle_analytics_helpers.rb
+++ b/spec/support/helpers/cycle_analytics_helpers.rb
@@ -3,6 +3,32 @@
module CycleAnalyticsHelpers
include GitHelpers
+ def wait_for_stages_to_load
+ expect(page).to have_selector '.js-stage-table'
+ wait_for_requests
+ end
+
+ def select_group(target_group)
+ visit group_analytics_cycle_analytics_path(target_group)
+
+ wait_for_stages_to_load
+ end
+
+ def toggle_dropdown(field)
+ page.within("[data-testid='#{field}']") do
+ find('.dropdown-toggle').click
+
+ wait_for_requests
+
+ expect(find('.dropdown-menu')).to have_selector('.dropdown-item')
+ end
+ end
+
+ def select_dropdown_option_by_value(name, value, elem = '.dropdown-item')
+ toggle_dropdown name
+ page.find("[data-testid='#{name}'] .dropdown-menu").find("#{elem}[value='#{value}']").click
+ end
+
def create_commit_referencing_issue(issue, branch_name: generate(:branch))
project.repository.add_branch(user, branch_name, 'master')
create_commit("Commit for ##{issue.iid}", issue.project, user, branch_name)
diff --git a/spec/support/helpers/features/members_table_helpers.rb b/spec/support/helpers/features/members_table_helpers.rb
index 5394e370900..4a0e218ed3e 100644
--- a/spec/support/helpers/features/members_table_helpers.rb
+++ b/spec/support/helpers/features/members_table_helpers.rb
@@ -30,6 +30,32 @@ module Spec
def invite_users_form
page.find('[data-testid="invite-users-form"]')
end
+
+ def find_row(name)
+ page.within(members_table) do
+ page.find('tbody > tr', text: name)
+ end
+ end
+
+ def find_member_row(user)
+ find_row(user.name)
+ end
+
+ def find_invited_member_row(email)
+ find_row(email)
+ end
+
+ def find_group_row(group)
+ find_row(group.full_name)
+ end
+
+ def fill_in_filtered_search(label, with:)
+ page.within '[data-testid="members-filtered-search-bar"]' do
+ find_field(label).click
+ find('input').native.send_keys(with)
+ click_button 'Search'
+ end
+ end
end
end
end
diff --git a/spec/support/helpers/features/notes_helpers.rb b/spec/support/helpers/features/notes_helpers.rb
index 8c27f81930d..f8252254531 100644
--- a/spec/support/helpers/features/notes_helpers.rb
+++ b/spec/support/helpers/features/notes_helpers.rb
@@ -21,6 +21,8 @@ module Spec
find(".js-comment-submit-button").click
end
end
+
+ wait_for_requests
end
def edit_note(note_text_to_edit, new_note_text)
@@ -29,6 +31,8 @@ module Spec
fill_in('note[note]', with: new_note_text)
find('.js-comment-button').click
end
+
+ wait_for_requests
end
def preview_note(text)
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index 35c298a4d48..46d0c13dc18 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -125,11 +125,15 @@ module GraphqlHelpers
end
def graphql_query_for(name, attributes = {}, fields = nil)
- <<~QUERY
- {
- #{query_graphql_field(name, attributes, fields)}
- }
- QUERY
+ type = GitlabSchema.types['Query'].fields[GraphqlHelpers.fieldnamerize(name)]&.type
+ wrap_query(query_graphql_field(name, attributes, fields, type))
+ end
+
+ def wrap_query(query)
+ q = query.to_s
+ return q if q.starts_with?('{')
+
+ "{ #{q} }"
end
def graphql_mutation(name, input, fields = nil, &block)
@@ -219,12 +223,13 @@ module GraphqlHelpers
"#{namerized}#{field_params}"
end
- def query_graphql_field(name, attributes = {}, fields = nil)
+ def query_graphql_field(name, attributes = {}, fields = nil, type = nil)
+ type ||= name.to_s.classify
attributes, fields = [nil, attributes] if fields.nil? && !attributes.is_a?(Hash)
field = field_with_params(name, attributes)
- field + wrap_fields(fields || all_graphql_fields_for(name.to_s.classify)).to_s
+ field + wrap_fields(fields || all_graphql_fields_for(type)).to_s
end
def page_info_selection
@@ -237,6 +242,10 @@ module GraphqlHelpers
query_graphql_path([[name, args], node_selection], fields)
end
+ def query_graphql_fragment(name)
+ "... on #{name} { #{all_graphql_fields_for(name)} }"
+ end
+
# e.g:
# query_graphql_path(%i[foo bar baz], all_graphql_fields_for('Baz'))
# => foo { bar { baz { x y z } } }
@@ -277,8 +286,8 @@ module GraphqlHelpers
allow_high_graphql_recursion
allow_high_graphql_transaction_threshold
- type = GitlabSchema.types[class_name.to_s]
- return "" unless type
+ type = class_name.respond_to?(:kind) ? class_name : GitlabSchema.types[class_name.to_s]
+ raise "#{class_name} is not a known type in the GitlabSchema" unless type
# We can't guess arguments, so skip fields that require them
skip = ->(name, field) { excluded.include?(name) || required_arguments?(field) }
@@ -287,7 +296,7 @@ module GraphqlHelpers
end
def with_signature(variables, query)
- %Q[query(#{variables.map(&:sig).join(', ')}) #{query}]
+ %Q[query(#{variables.map(&:sig).join(', ')}) #{wrap_query(query)}]
end
def var(type)
@@ -305,6 +314,10 @@ module GraphqlHelpers
def post_graphql(query, current_user: nil, variables: nil, headers: {})
params = { query: query, variables: serialize_variables(variables) }
post api('/', current_user, version: 'graphql'), params: params, headers: headers
+
+ if graphql_errors # Errors are acceptable, but not this one:
+ expect(graphql_errors).not_to include(a_hash_including('message' => 'Internal server error'))
+ end
end
def post_graphql_mutation(mutation, current_user: nil)
@@ -374,10 +387,8 @@ module GraphqlHelpers
end
# Raises an error if no data is found
- def graphql_data(body = json_response)
- # Note that `json_response` is defined as `let(:json_response)` and
- # therefore, in a spec with multiple queries, will only contain data
- # from the _first_ query, not subsequent ones
+ # NB: We use fresh_response_data to support tests that make multiple requests.
+ def graphql_data(body = fresh_response_data)
body['data'] || (raise NoData, graphql_errors(body))
end
@@ -510,8 +521,12 @@ module GraphqlHelpers
end
end
- def global_id_of(model)
- model.to_global_id.to_s
+ def global_id_of(model, id: nil, model_name: nil)
+ if id || model_name
+ ::Gitlab::GlobalId.build(model, id: id, model_name: model_name).to_s
+ else
+ model.to_global_id.to_s
+ end
end
def missing_required_argument(path, argument)
diff --git a/spec/support/helpers/next_found_instance_of.rb b/spec/support/helpers/next_found_instance_of.rb
index ff34fcdd1d3..feb63f90211 100644
--- a/spec/support/helpers/next_found_instance_of.rb
+++ b/spec/support/helpers/next_found_instance_of.rb
@@ -6,7 +6,7 @@ module NextFoundInstanceOf
def expect_next_found_instance_of(klass)
check_if_active_record!(klass)
- stub_allocate(expect(klass)) do |expectation|
+ stub_allocate(expect(klass), klass) do |expectation|
yield(expectation)
end
end
@@ -14,7 +14,7 @@ module NextFoundInstanceOf
def allow_next_found_instance_of(klass)
check_if_active_record!(klass)
- stub_allocate(allow(klass)) do |allowance|
+ stub_allocate(allow(klass), klass) do |allowance|
yield(allowance)
end
end
@@ -25,9 +25,17 @@ module NextFoundInstanceOf
raise ArgumentError.new(ERROR_MESSAGE) unless klass < ActiveRecord::Base
end
- def stub_allocate(target)
+ def stub_allocate(target, klass)
target.to receive(:allocate).and_wrap_original do |method|
- method.call.tap { |allocation| yield(allocation) }
+ method.call.tap do |allocation|
+ # ActiveRecord::Core.allocate returns a frozen object:
+ # https://github.com/rails/rails/blob/291a3d2ef29a3842d1156ada7526f4ee60dd2b59/activerecord/lib/active_record/core.rb#L620
+ # It's unexpected behavior and probably a bug in Rails
+ # Let's work it around by setting the attributes to default to unfreeze the object for now
+ allocation.instance_variable_set(:@attributes, klass._default_attributes)
+
+ yield(allocation)
+ end
end
end
end
diff --git a/spec/support/helpers/search_settings_helpers.rb b/spec/support/helpers/search_settings_helpers.rb
new file mode 100644
index 00000000000..838f897bff5
--- /dev/null
+++ b/spec/support/helpers/search_settings_helpers.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+module SearchHelpers
+ self::INPUT_PLACEHOLDER = 'Search settings'
+end
diff --git a/spec/support/helpers/seed_helper.rb b/spec/support/helpers/seed_helper.rb
index 90d7f60fdeb..f65993efa05 100644
--- a/spec/support/helpers/seed_helper.rb
+++ b/spec/support/helpers/seed_helper.rb
@@ -4,7 +4,7 @@ require_relative 'test_env'
# This file is specific to specs in spec/lib/gitlab/git/
-SEED_STORAGE_PATH = TestEnv.repos_path
+SEED_STORAGE_PATH = Gitlab::GitalyClient::StorageSettings.allow_disk_access { TestEnv.repos_path }
TEST_REPO_PATH = 'gitlab-git-test.git'
TEST_NORMAL_REPO_PATH = 'not-bare-repo.git'
TEST_MUTABLE_REPO_PATH = 'mutable-repo.git'
diff --git a/spec/support/helpers/smime_helper.rb b/spec/support/helpers/smime_helper.rb
index 261aef9518e..fa16c433c6b 100644
--- a/spec/support/helpers/smime_helper.rb
+++ b/spec/support/helpers/smime_helper.rb
@@ -52,7 +52,7 @@ module SmimeHelper
cert.add_extension(extension_factory.create_extension('extendedKeyUsage', 'clientAuth,emailProtection', false))
end
- cert.sign(signed_by&.fetch(:key, nil) || key, OpenSSL::Digest::SHA256.new)
+ cert.sign(signed_by&.fetch(:key, nil) || key, OpenSSL::Digest.new('SHA256'))
{ key: key, cert: cert }
end
diff --git a/spec/support/helpers/sorting_helper.rb b/spec/support/helpers/sorting_helper.rb
index 3801d25fb63..f19f8c12928 100644
--- a/spec/support/helpers/sorting_helper.rb
+++ b/spec/support/helpers/sorting_helper.rb
@@ -17,4 +17,35 @@ module SortingHelper
click_link value
end
end
+
+ def nils_last(value)
+ NilsLast.new(value)
+ end
+
+ class NilsLast
+ include Comparable
+
+ attr_reader :value
+ delegate :==, :eql?, :hash, to: :value
+
+ def initialize(value)
+ @value = value
+ @reverse = false
+ end
+
+ def <=>(other)
+ return unless other.is_a?(self.class)
+ return 0 if value.nil? && other.value.nil?
+ return 1 if value.nil?
+ return -1 if other.value.nil?
+
+ int = value <=> other.value
+ @reverse ? -int : int
+ end
+
+ def -@
+ @reverse = true
+ self
+ end
+ end
end
diff --git a/spec/support/helpers/stub_configuration.rb b/spec/support/helpers/stub_configuration.rb
index 3b733a2e57a..9851a3de9e9 100644
--- a/spec/support/helpers/stub_configuration.rb
+++ b/spec/support/helpers/stub_configuration.rb
@@ -121,6 +121,12 @@ module StubConfiguration
allow(::Gitlab.config.packages).to receive_messages(to_settings(messages))
end
+ def stub_maintenance_mode_setting(value)
+ allow(Gitlab::CurrentSettings).to receive(:current_application_settings?).and_return(true)
+
+ stub_application_setting(maintenance_mode: value)
+ end
+
private
# Modifies stubbed messages to also stub possible predicate versions
diff --git a/spec/support/helpers/stub_object_storage.rb b/spec/support/helpers/stub_object_storage.rb
index dc54a21d0fa..0d0ac171baa 100644
--- a/spec/support/helpers/stub_object_storage.rb
+++ b/spec/support/helpers/stub_object_storage.rb
@@ -85,6 +85,13 @@ module StubObjectStorage
**params)
end
+ def stub_composer_cache_object_storage(**params)
+ stub_object_storage_uploader(config: Gitlab.config.packages.object_store,
+ uploader: ::Packages::Composer::CacheUploader,
+ remote_directory: 'packages',
+ **params)
+ end
+
def stub_uploads_object_storage(uploader = described_class, **params)
stub_object_storage_uploader(config: Gitlab.config.uploads.object_store,
uploader: uploader,
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index cb25f5f9429..2d71662b0eb 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -149,7 +149,9 @@ module TestEnv
end
end
- FileUtils.mkdir_p(repos_path)
+ FileUtils.mkdir_p(
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access { TestEnv.repos_path }
+ )
FileUtils.mkdir_p(SECOND_STORAGE_PATH)
FileUtils.mkdir_p(backup_path)
FileUtils.mkdir_p(pages_path)
diff --git a/spec/support/helpers/wait_for_requests.rb b/spec/support/helpers/wait_for_requests.rb
index 43060e571a9..8fd9bb47053 100644
--- a/spec/support/helpers/wait_for_requests.rb
+++ b/spec/support/helpers/wait_for_requests.rb
@@ -52,6 +52,6 @@ module WaitForRequests
end
def finished_all_ajax_requests?
- Capybara.page.evaluate_script('window.pendingRequests || window.pendingRailsUJSRequests || 0').zero? # rubocop:disable Style/NumericPredicate
+ Capybara.page.evaluate_script('window.pendingRequests || window.pendingApolloRequests || window.pendingRailsUJSRequests || 0').zero? # rubocop:disable Style/NumericPredicate
end
end
diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb
index 6e75fa58700..47cffad8c41 100644
--- a/spec/support/matchers/markdown_matchers.rb
+++ b/spec/support/matchers/markdown_matchers.rb
@@ -246,6 +246,33 @@ module MarkdownMatchers
end
end
end
+
+ # MermaidFilter
+ matcher :parse_mermaid do
+ set_default_markdown_messages
+
+ match do |actual|
+ expect(actual).to have_selector('code.js-render-mermaid')
+ end
+ end
+
+ # PLantumlFilter
+ matcher :parse_plantuml do
+ set_default_markdown_messages
+
+ match do |actual|
+ expect(actual).to have_link(href: 'http://localhost:8080/png/U9npoazIqBLJ24uiIbImKl18pSd9vm80EtS5lW00')
+ end
+ end
+
+ # KrokiFilter
+ matcher :parse_kroki do
+ set_default_markdown_messages
+
+ match do |actual|
+ expect(actual).to have_link(href: 'http://localhost:8000/nomnoml/svg/eNqLDsgsSixJrUmtTHXOL80rsVLwzCupKUrMTNHQtC7IzMlJTE_V0KzhUlCITkpNLEqJ1dWNLkgsKsoviUUSs7KLTssvzVHIzS8tyYjligUAMhEd0g==')
+ end
+ end
end
# Monkeypatch the matcher DSL so that we can reduce some noisy duplication for
diff --git a/spec/support/matchers/nullify_if_blank_matcher.rb b/spec/support/matchers/nullify_if_blank_matcher.rb
new file mode 100644
index 00000000000..8f6737499cc
--- /dev/null
+++ b/spec/support/matchers/nullify_if_blank_matcher.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+RSpec::Matchers.define :nullify_if_blank do |attribute|
+ match do |record|
+ expect(record.class.attributes_to_nullify).to include(attribute)
+ end
+
+ failure_message do |record|
+ "expected nullify_if_blank configuration on #{record.class} to include #{attribute}"
+ end
+end
diff --git a/spec/support/matchers/pushed_frontend_feature_flags_matcher.rb b/spec/support/matchers/pushed_frontend_feature_flags_matcher.rb
new file mode 100644
index 00000000000..b49d4da8cda
--- /dev/null
+++ b/spec/support/matchers/pushed_frontend_feature_flags_matcher.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+RSpec::Matchers.define :have_pushed_frontend_feature_flags do |expected|
+ def to_js(key, value)
+ "\"#{key}\":#{value}"
+ end
+
+ match do |actual|
+ expected.all? do |feature_flag_name, enabled|
+ page.html.include?(to_js(feature_flag_name, enabled))
+ end
+ end
+
+ failure_message do |actual|
+ missing = expected.select do |feature_flag_name, enabled|
+ !page.html.include?(to_js(feature_flag_name, enabled))
+ end
+
+ formatted_missing_flags = missing.map { |feature_flag_name, enabled| to_js(feature_flag_name, enabled) }.join("\n")
+
+ "The following feature flag(s) cannot be found in the frontend HTML source: #{formatted_missing_flags}"
+ end
+end
diff --git a/spec/support/matchers/track_self_describing_event_matcher.rb b/spec/support/matchers/track_self_describing_event_matcher.rb
new file mode 100644
index 00000000000..c3723d2418f
--- /dev/null
+++ b/spec/support/matchers/track_self_describing_event_matcher.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+RSpec::Matchers.define :track_self_describing_event do |schema, data|
+ match do
+ expect(Gitlab::Tracking).to have_received(:self_describing_event)
+ .with(schema, data: data)
+ end
+
+ match_when_negated do
+ expect(Gitlab::Tracking).not_to have_received(:self_describing_event)
+ end
+end
diff --git a/spec/support/memory_instrumentation_helper.rb b/spec/support/memory_instrumentation_helper.rb
new file mode 100644
index 00000000000..84ec02fa5aa
--- /dev/null
+++ b/spec/support/memory_instrumentation_helper.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+# Memory instrumentation can only be done if running on a valid Ruby
+#
+# This concept is currently tried to be upstreamed here:
+# - https://github.com/ruby/ruby/pull/3978
+module MemoryInstrumentationHelper
+ def skip_memory_instrumentation!
+ return if ::Gitlab::Memory::Instrumentation.available?
+
+ # if we are running in CI, a test cannot be skipped
+ return if ENV['CI']
+
+ skip 'Missing a memory instrumentation patch. ' \
+ 'More information can be found here: https://gitlab.com/gitlab-org/gitlab/-/issues/296530.'
+ end
+end
diff --git a/spec/support/migrations_helpers/vulnerabilities_findings_helper.rb b/spec/support/migrations_helpers/vulnerabilities_findings_helper.rb
new file mode 100644
index 00000000000..922f49ba84a
--- /dev/null
+++ b/spec/support/migrations_helpers/vulnerabilities_findings_helper.rb
@@ -0,0 +1,118 @@
+# frozen_string_literal: true
+
+module MigrationHelpers
+ module VulnerabilitiesFindingsHelper
+ def attributes_for_vulnerabilities_finding
+ uuid = SecureRandom.uuid
+
+ {
+ project_fingerprint: SecureRandom.hex(20),
+ location_fingerprint: Digest::SHA1.hexdigest(SecureRandom.hex(10)),
+ uuid: uuid,
+ name: "Vulnerability Finding #{uuid}",
+ metadata_version: '1.3',
+ raw_metadata: raw_metadata
+ }
+ end
+
+ def raw_metadata
+ {
+ "description" => "The cipher does not provide data integrity update 1",
+ "message" => "The cipher does not provide data integrity",
+ "cve" => "818bf5dacb291e15d9e6dc3c5ac32178:CIPHER",
+ "solution" => "GCM mode introduces an HMAC into the resulting encrypted data, providing integrity of the result.",
+ "location" => {
+ "file" => "maven/src/main/java/com/gitlab/security_products/tests/App.java",
+ "start_line" => 29,
+ "end_line" => 29,
+ "class" => "com.gitlab.security_products.tests.App",
+ "method" => "insecureCypher"
+ },
+ "links" => [
+ {
+ "name" => "Cipher does not check for integrity first?",
+ "url" => "https://crypto.stackexchange.com/questions/31428/pbewithmd5anddes-cipher-does-not-check-for-integrity-first"
+ }
+ ],
+ "assets" => [
+ {
+ "type" => "postman",
+ "name" => "Test Postman Collection",
+ "url" => "http://localhost/test.collection"
+ }
+ ],
+ "evidence" => {
+ "summary" => "Credit card detected",
+ "request" => {
+ "method" => "GET",
+ "url" => "http://goat:8080/WebGoat/logout",
+ "body" => nil,
+ "headers" => [
+ {
+ "name" => "Accept",
+ "value" => "*/*"
+ }
+ ]
+ },
+ "response" => {
+ "reason_phrase" => "OK",
+ "status_code" => 200,
+ "body" => nil,
+ "headers" => [
+ {
+ "name" => "Content-Length",
+ "value" => "0"
+ }
+ ]
+ },
+ "source" => {
+ "id" => "assert:Response Body Analysis",
+ "name" => "Response Body Analysis",
+ "url" => "htpp://hostname/documentation"
+ },
+ "supporting_messages" => [
+ {
+ "name" => "Origional",
+ "request" => {
+ "method" => "GET",
+ "url" => "http://goat:8080/WebGoat/logout",
+ "body" => "",
+ "headers" => [
+ {
+ "name" => "Accept",
+ "value" => "*/*"
+ }
+ ]
+ }
+ },
+ {
+ "name" => "Recorded",
+ "request" => {
+ "method" => "GET",
+ "url" => "http://goat:8080/WebGoat/logout",
+ "body" => "",
+ "headers" => [
+ {
+ "name" => "Accept",
+ "value" => "*/*"
+ }
+ ]
+ },
+ "response" => {
+ "reason_phrase" => "OK",
+ "status_code" => 200,
+ "body" => "",
+ "headers" => [
+ {
+ "name" => "Content-Length",
+ "value" => "0"
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ end
+ end
+end
diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb
index 57d8320b76a..3fd4f2698e9 100644
--- a/spec/support/shared_contexts/navbar_structure_context.rb
+++ b/spec/support/shared_contexts/navbar_structure_context.rb
@@ -18,7 +18,8 @@ RSpec.shared_context 'project navbar structure' do
{
nav_item: _('Security & Compliance'),
nav_sub_items: [
- _('Audit Events')
+ _('Configuration'),
+ (_('Audit Events') if Gitlab.ee?)
]
}
end
@@ -71,7 +72,7 @@ RSpec.shared_context 'project navbar structure' do
_('Schedules')
]
},
- (security_and_compliance_nav_item if Gitlab.ee?),
+ security_and_compliance_nav_item,
{
nav_item: _('Operations'),
nav_sub_items: [
@@ -190,7 +191,7 @@ RSpec.shared_context 'group navbar structure' do
nav_item: _('Merge Requests'),
nav_sub_items: []
},
- (security_and_compliance_nav_item if Gitlab.ee?),
+ security_and_compliance_nav_item,
(push_rules_nav_item if Gitlab.ee?),
{
nav_item: _('Kubernetes'),
diff --git a/spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb b/spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb
index 580ebf00dcb..f3bbb325475 100644
--- a/spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb
+++ b/spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb
@@ -22,7 +22,7 @@ RSpec.shared_context 'conan api setup' do
let(:jwt_secret) do
OpenSSL::HMAC.hexdigest(
- OpenSSL::Digest::SHA256.new,
+ OpenSSL::Digest.new('SHA256'),
base_secret,
Gitlab::ConanToken::HMAC_KEY
)
@@ -67,9 +67,9 @@ RSpec.shared_context 'conan file upload endpoints' do
include WorkhorseHelpers
include HttpBasicAuthHelpers
+ include_context 'workhorse headers'
+
let(:jwt) { build_jwt(personal_access_token) }
- let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
- let(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
- let(:headers_with_token) { build_token_auth_header(jwt.encoded).merge(workhorse_header) }
+ let(:headers_with_token) { build_token_auth_header(jwt.encoded).merge(workhorse_headers) }
let(:recipe_path) { "foo/bar/#{project.full_path.tr('/', '+')}/baz"}
end
diff --git a/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb b/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb
index 7c23ec33cf8..60a29d78084 100644
--- a/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb
+++ b/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb
@@ -4,10 +4,10 @@ RSpec.shared_context 'npm api setup' do
include PackagesManagerApiSpecHelpers
include HttpBasicAuthHelpers
- let_it_be(:user) { create(:user) }
+ let_it_be(:user, reload: true) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project, reload: true) { create(:project, :public, namespace: group) }
- let_it_be(:package, reload: true) { create(:npm_package, project: project) }
+ let_it_be(:package, reload: true) { create(:npm_package, project: project, name: "@#{group.path}/scoped_package") }
let_it_be(:token) { create(:oauth_access_token, scopes: 'api', resource_owner: user) }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
let_it_be(:job, reload: true) { create(:ci_build, user: user, status: :running) }
@@ -15,8 +15,15 @@ RSpec.shared_context 'npm api setup' do
let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
let(:package_name) { package.name }
+end
- before do
- project.add_developer(user)
+RSpec.shared_context 'set package name from package name type' do
+ let(:package_name) do
+ case package_name_type
+ when :scoped_naming_convention
+ "@#{group.path}/scoped-package"
+ when :non_existing
+ 'non-existing-package'
+ end
end
end
diff --git a/spec/support/shared_contexts/requests/api/workhorse_shared_context.rb b/spec/support/shared_contexts/requests/api/workhorse_shared_context.rb
new file mode 100644
index 00000000000..36be64339a2
--- /dev/null
+++ b/spec/support/shared_contexts/requests/api/workhorse_shared_context.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'workhorse headers' do
+ let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
+ let(:workhorse_headers) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
+end
diff --git a/spec/support/shared_contexts/services_shared_context.rb b/spec/support/shared_contexts/services_shared_context.rb
index 320f7564cf9..3322c6ef01a 100644
--- a/spec/support/shared_contexts/services_shared_context.rb
+++ b/spec/support/shared_contexts/services_shared_context.rb
@@ -62,7 +62,6 @@ Service.available_services_names.each do |service|
stub_licensed_features(licensed_feature => true)
project.clear_memoization(:disabled_services)
- project.clear_memoization(:licensed_feature_available)
end
end
end
diff --git a/spec/support/shared_examples/alert_notification_service_shared_examples.rb b/spec/support/shared_examples/alert_notification_service_shared_examples.rb
index 1568e4357a1..7bd6df8c608 100644
--- a/spec/support/shared_examples/alert_notification_service_shared_examples.rb
+++ b/spec/support/shared_examples/alert_notification_service_shared_examples.rb
@@ -3,7 +3,7 @@
RSpec.shared_examples 'Alert Notification Service sends notification email' do
let(:notification_service) { spy }
- it 'sends a notification for firing alerts only' do
+ it 'sends a notification' do
expect(NotificationService)
.to receive(:new)
.and_return(notification_service)
@@ -15,15 +15,15 @@ RSpec.shared_examples 'Alert Notification Service sends notification email' do
end
end
-RSpec.shared_examples 'Alert Notification Service sends no notifications' do |http_status:|
- let(:notification_service) { spy }
- let(:create_events_service) { spy }
-
+RSpec.shared_examples 'Alert Notification Service sends no notifications' do |http_status: nil|
it 'does not notify' do
- expect(notification_service).not_to receive(:async)
- expect(create_events_service).not_to receive(:execute)
+ expect(NotificationService).not_to receive(:new)
- expect(subject).to be_error
- expect(subject.http_status).to eq(http_status)
+ if http_status.present?
+ expect(subject).to be_error
+ expect(subject.http_status).to eq(http_status)
+ else
+ expect(subject).to be_success
+ end
end
end
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 f89d52f81ad..7f49d20c83e 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,8 @@
RSpec.shared_examples 'multiple issue boards' do
context 'authorized user' do
before do
+ stub_feature_flags(board_new_list: false)
+
parent.add_maintainer(user)
login_as(user)
diff --git a/spec/support/shared_examples/controllers/unique_hll_events_examples.rb b/spec/support/shared_examples/controllers/unique_hll_events_examples.rb
index c5d65743810..842ad89bafd 100644
--- a/spec/support/shared_examples/controllers/unique_hll_events_examples.rb
+++ b/spec/support/shared_examples/controllers/unique_hll_events_examples.rb
@@ -5,20 +5,14 @@
# - expected_type
# - target_id
-RSpec.shared_examples 'tracking unique hll events' do |feature_flag|
+RSpec.shared_examples 'tracking unique hll events' do
it 'tracks unique event' do
- expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event).with(target_id, values: expected_type)
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter).to(
+ receive(:track_event)
+ .with(target_id, values: expected_type)
+ .and_call_original # we call original to trigger additional validations; otherwise the method is stubbed
+ )
request
end
-
- context 'when feature flag is disabled' do
- it 'does not track unique event' do
- stub_feature_flags(feature_flag => false)
-
- expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event)
-
- request
- end
- end
end
diff --git a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
index dcbf494186a..0a040557ffe 100644
--- a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
@@ -218,7 +218,7 @@ RSpec.shared_examples 'wiki controller actions' do
end
context 'page view tracking' do
- it_behaves_like 'tracking unique hll events', :track_unique_wiki_page_views do
+ it_behaves_like 'tracking unique hll events' do
let(:target_id) { 'wiki_action' }
let(:expected_type) { instance_of(String) }
end
diff --git a/spec/support/shared_examples/features/comment_and_close_button_shared_examples.rb b/spec/support/shared_examples/features/comment_and_close_button_shared_examples.rb
new file mode 100644
index 00000000000..4ee2840ed9f
--- /dev/null
+++ b/spec/support/shared_examples/features/comment_and_close_button_shared_examples.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'page with comment and close button' do |button_text|
+ context 'when remove_comment_close_reopen feature flag is enabled' do
+ before do
+ stub_feature_flags(remove_comment_close_reopen: true)
+ setup
+ end
+
+ it "does not show #{button_text} button" do
+ within '.note-form-actions' do
+ expect(page).not_to have_button(button_text)
+ end
+ end
+ end
+
+ context 'when remove_comment_close_reopen feature flag is disabled' do
+ before do
+ stub_feature_flags(remove_comment_close_reopen: false)
+ setup
+ end
+
+ it "shows #{button_text} button" do
+ within '.note-form-actions' do
+ expect(page).to have_button(button_text)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/discussion_comments_shared_example.rb b/spec/support/shared_examples/features/discussion_comments_shared_example.rb
index 560cfbfb117..6bebd59ed70 100644
--- a/spec/support/shared_examples/features/discussion_comments_shared_example.rb
+++ b/spec/support/shared_examples/features/discussion_comments_shared_example.rb
@@ -150,12 +150,13 @@ RSpec.shared_examples 'thread comments' do |resource_name|
wait_for_requests
end
- it 'clicking "Start thread" will post a thread' do
+ it 'clicking "Start thread" will post a thread and show a reply component' do
expect(page).to have_content(comment)
new_comment = all(comments_selector).last
expect(new_comment).to have_selector('.discussion')
+ expect(new_comment).to have_css('.discussion-with-resolve-btn')
end
if resource_name =~ /(issue|merge request)/
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..ccd063faac4 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
@@ -48,7 +48,7 @@ RSpec.shared_examples 'an editable merge request' do
end
page.within '.reviewer' do
- expect(page).to have_content user.name
+ expect(page).to have_content user.username
end
page.within '.milestone' do
diff --git a/spec/support/shared_examples/features/multiple_reviewers_mr_shared_examples.rb b/spec/support/shared_examples/features/multiple_reviewers_mr_shared_examples.rb
index 48cde90bd9b..ad6ca3e1900 100644
--- a/spec/support/shared_examples/features/multiple_reviewers_mr_shared_examples.rb
+++ b/spec/support/shared_examples/features/multiple_reviewers_mr_shared_examples.rb
@@ -40,7 +40,7 @@ RSpec.shared_examples 'multiple reviewers merge request' do |action, save_button
# Closing dropdown to persist
click_link 'Edit'
- expect(page).to have_content user2.name
+ expect(page).to have_content user2.username
end
end
end
diff --git a/spec/support/shared_examples/features/navbar_shared_examples.rb b/spec/support/shared_examples/features/navbar_shared_examples.rb
index c768e95c45a..9b89a3b5e54 100644
--- a/spec/support/shared_examples/features/navbar_shared_examples.rb
+++ b/spec/support/shared_examples/features/navbar_shared_examples.rb
@@ -8,12 +8,13 @@ RSpec.shared_examples 'verified navigation bar' do
end
it 'renders correctly' do
- current_structure = page.all('.sidebar-top-level-items > li', class: ['!hidden']).map do |item|
+ # we are using * here in the selectors to prevent a regression where we added a non 'li' inside an 'ul'
+ current_structure = page.all('.sidebar-top-level-items > *', class: ['!hidden']).map do |item|
next if item.find_all('a').empty?
nav_item = item.find_all('a').first.text.gsub(/\s+\d+$/, '') # remove counts at the end
- nav_sub_items = item.all('.sidebar-sub-level-items > li', class: ['!fly-out-top-item']).map do |list_item|
+ nav_sub_items = item.all('.sidebar-sub-level-items > *', class: ['!fly-out-top-item']).map do |list_item|
list_item.all('a').first.text
end
diff --git a/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb b/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb
index a46382bc292..56154c7cd03 100644
--- a/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb
+++ b/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb
@@ -56,6 +56,8 @@ RSpec.shared_examples "protected branches > access control > CE" do
expect(first("li")).to have_content("Roles")
find(:link, access_type_name).click
end
+
+ find(".js-allowed-to-push").click
end
wait_for_requests
diff --git a/spec/support/shared_examples/features/protected_branches_with_deploy_keys_examples.rb b/spec/support/shared_examples/features/protected_branches_with_deploy_keys_examples.rb
index a2d2143271c..28fe198c9c3 100644
--- a/spec/support/shared_examples/features/protected_branches_with_deploy_keys_examples.rb
+++ b/spec/support/shared_examples/features/protected_branches_with_deploy_keys_examples.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
-RSpec.shared_examples 'when the deploy_keys_on_protected_branches FF is turned on' do
+RSpec.shared_examples 'Deploy keys with protected branches' do
before do
- stub_feature_flags(deploy_keys_on_protected_branches: true)
project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/support/shared_examples/features/search_settings_shared_examples.rb b/spec/support/shared_examples/features/search_settings_shared_examples.rb
new file mode 100644
index 00000000000..6a507c4be56
--- /dev/null
+++ b/spec/support/shared_examples/features/search_settings_shared_examples.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'cannot search settings' do
+ it 'does note have search settings field' do
+ expect(page).not_to have_field(placeholder: SearchHelpers::INPUT_PLACEHOLDER)
+ end
+end
+
+RSpec.shared_examples 'can search settings' do |search_term, non_match_section|
+ it 'has search settings field' do
+ expect(page).to have_field(placeholder: SearchHelpers::INPUT_PLACEHOLDER)
+ end
+
+ it 'hides unmatching sections on search' do
+ expect(page).to have_content(non_match_section)
+
+ fill_in SearchHelpers::INPUT_PLACEHOLDER, with: search_term
+
+ expect(page).to have_content(search_term)
+ expect(page).not_to have_content(non_match_section)
+ end
+end
+
+RSpec.shared_examples 'can search settings with feature flag check' do |search_term, non_match_section|
+ let(:flag) { true }
+
+ before do
+ stub_feature_flags(search_settings_in_page: flag)
+
+ visit(visit_path)
+ end
+
+ context 'with feature flag on' do
+ it_behaves_like 'can search settings', search_term, non_match_section
+ end
+
+ context 'with feature flag off' do
+ let(:flag) { false }
+
+ it_behaves_like 'cannot search settings'
+ end
+end
diff --git a/spec/support/shared_examples/finders/packages_shared_examples.rb b/spec/support/shared_examples/finders/packages_shared_examples.rb
index 52976565b21..2d4e8d0df1f 100644
--- a/spec/support/shared_examples/finders/packages_shared_examples.rb
+++ b/spec/support/shared_examples/finders/packages_shared_examples.rb
@@ -17,3 +17,23 @@ RSpec.shared_examples 'concerning versionless param' do
it { is_expected.not_to include(versionless_package) }
end
end
+
+RSpec.shared_examples 'concerning package statuses' do
+ let_it_be(:hidden_package) { create(:maven_package, :hidden, project: project) }
+
+ context 'hidden packages' do
+ it { is_expected.not_to include(hidden_package) }
+ end
+
+ context 'with status param' do
+ let(:params) { { status: :hidden } }
+
+ it { is_expected.to match_array([hidden_package]) }
+ end
+
+ context 'with invalid status param' do
+ let(:params) { { status: 'invalid_status' } }
+
+ it { expect { subject }.to raise_exception(described_class::InvalidStatusError) }
+ end
+end
diff --git a/spec/support/shared_examples/graphql/label_fields.rb b/spec/support/shared_examples/graphql/label_fields.rb
index caf5dae409a..4159e4e03ab 100644
--- a/spec/support/shared_examples/graphql/label_fields.rb
+++ b/spec/support/shared_examples/graphql/label_fields.rb
@@ -18,7 +18,7 @@ RSpec.shared_examples 'a GraphQL type with labels' do
subject { described_class.fields['labels'] }
it { is_expected.to have_graphql_type(Types::LabelType.connection_type) }
- it { is_expected.to have_graphql_arguments(:search_term) }
+ it { is_expected.to have_graphql_arguments(labels_resolver_arguments) }
end
end
@@ -105,7 +105,7 @@ RSpec.shared_examples 'querying a GraphQL type with labels' do
run_query(query_for(label_a))
end
- it 'batches queries for labels by title' do
+ it 'batches queries for labels by title', :request_store do
multi_selection = query_for(label_b, label_c)
single_selection = query_for(label_d)
diff --git a/spec/support/shared_examples/graphql/mutations/boards_list_create_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/boards_list_create_shared_examples.rb
new file mode 100644
index 00000000000..b096a5e17c0
--- /dev/null
+++ b/spec/support/shared_examples/graphql/mutations/boards_list_create_shared_examples.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples 'board lists create mutation' do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+
+ let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
+ let(:list_create_params) { {} }
+
+ subject { mutation.resolve(board_id: board.to_global_id.to_s, **list_create_params) }
+
+ describe '#ready?' do
+ it 'raises an error if required arguments are missing' do
+ expect { mutation.ready?(board_id: 'some id') }
+ .to raise_error(Gitlab::Graphql::Errors::ArgumentError, /one and only one of/)
+ end
+
+ it 'raises an error if too many required arguments are specified' do
+ expect { mutation.ready?(board_id: 'some id', backlog: true, label_id: 'some label') }
+ .to raise_error(Gitlab::Graphql::Errors::ArgumentError, /one and only one of/)
+ end
+ end
+
+ describe '#resolve' do
+ context 'with proper permissions' do
+ before_all do
+ group.add_reporter(user)
+ end
+
+ describe 'backlog list' do
+ let(:list_create_params) { { backlog: true } }
+
+ it 'creates one and only one backlog' do
+ expect { subject }.to change { board.lists.backlog.count }.by(1)
+ expect(board.lists.backlog.first.list_type).to eq 'backlog'
+
+ backlog_id = board.lists.backlog.first.id
+
+ expect { subject }.not_to change { board.lists.backlog.count }
+ expect(board.lists.backlog.last.id).to eq backlog_id
+ end
+ end
+
+ describe 'label list' do
+ let_it_be(:dev_label) do
+ create(:group_label, title: 'Development', color: '#FFAABB', group: group)
+ end
+
+ let(:list_create_params) { { label_id: dev_label.to_global_id.to_s } }
+
+ it 'creates a new label board list' do
+ expect { subject }.to change { board.lists.count }.by(1)
+
+ new_list = subject[:list]
+
+ expect(new_list.title).to eq dev_label.title
+ expect(new_list.position).to eq 0
+ end
+
+ context 'when label not found' do
+ let(:list_create_params) { { label_id: "gid://gitlab/Label/#{non_existing_record_id}" } }
+
+ it 'returns an error' do
+ expect(subject[:errors]).to include 'Label not found'
+ end
+ end
+ end
+ end
+
+ context 'without proper permissions' do
+ before_all do
+ group.add_guest(user)
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb b/spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb
new file mode 100644
index 00000000000..d294f034d2e
--- /dev/null
+++ b/spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples 'a mutation which can mutate a spammable' do
+ describe "#additional_spam_params" do
+ it 'passes additional spam params to the service' do
+ args = [
+ anything,
+ anything,
+ hash_including(
+ api: true,
+ request: instance_of(ActionDispatch::Request),
+ captcha_response: captcha_response,
+ spam_log_id: spam_log_id
+ )
+ ]
+ expect(service).to receive(:new).with(*args).and_call_original
+
+ subject
+ end
+ end
+
+ describe "#with_spam_action_fields" do
+ it 'resolves with spam action fields' do
+ subject
+
+ # NOTE: We do not need to assert on the specific values of spam action fields here, we only need
+ # to verify that #with_spam_action_fields was invoked and that the fields are present in the
+ # response. The specific behavior of #with_spam_action_fields is covered in the
+ # CanMutateSpammable unit tests.
+ expect(mutation_response.keys)
+ .to include('spam', 'spamLogId', 'needsCaptchaResponse', 'captchaSiteKey')
+ end
+ end
+end
diff --git a/spec/support/shared_examples/graphql/mutations/http_integrations_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/http_integrations_shared_examples.rb
index 0338eb43f8d..4468af1a603 100644
--- a/spec/support/shared_examples/graphql/mutations/http_integrations_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/mutations/http_integrations_shared_examples.rb
@@ -17,3 +17,78 @@ RSpec.shared_examples 'creating a new HTTP integration' do
expect(integration_response['apiUrl']).to eq(nil)
end
end
+
+RSpec.shared_examples 'updating an existing HTTP integration' do
+ it 'updates the integration' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ integration_response = mutation_response['integration']
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(integration_response['id']).to eq(GitlabSchema.id_from_object(integration).to_s)
+ expect(integration_response['name']).to eq('Modified Name')
+ expect(integration_response['active']).to be_falsey
+ expect(integration_response['url']).to include('modified-name')
+ end
+end
+
+RSpec.shared_examples 'validating the payload_example' do
+ context 'with invalid payloadExample attribute' do
+ let(:payload_example) { 'not a JSON' }
+
+ it 'responds with errors' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect_graphql_errors_to_include(/was provided invalid value for payloadExample \(Invalid JSON string/)
+ end
+ end
+
+ it 'validates the payload_example size' do
+ allow(::Gitlab::Utils::DeepSize)
+ .to receive(:new)
+ .with(Gitlab::Json.parse(payload_example))
+ .and_return(double(valid?: false))
+
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect_graphql_errors_to_include(/payloadExample JSON is too big/)
+ end
+end
+
+RSpec.shared_examples 'validating the payload_attribute_mappings' do
+ context 'with invalid payloadAttributeMapping attribute does not contain fieldName' do
+ let(:payload_attribute_mappings) do
+ [{ path: %w[alert name], type: 'STRING' }]
+ end
+
+ it 'responds with errors' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect_graphql_errors_to_include(/was provided invalid value for payloadAttributeMappings\.0\.fieldName \(Expected value to not be null/)
+ end
+ end
+
+ context 'with invalid payloadAttributeMapping attribute does not contain path' do
+ let(:payload_attribute_mappings) do
+ [{ fieldName: 'TITLE', type: 'STRING' }]
+ end
+
+ it 'responds with errors' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect_graphql_errors_to_include(/was provided invalid value for payloadAttributeMappings\.0\.path \(Expected value to not be null/)
+ end
+ end
+
+ context 'with invalid payloadAttributeMapping attribute does not contain type' do
+ let(:payload_attribute_mappings) do
+ [{ fieldName: 'TITLE', path: %w[alert name] }]
+ end
+
+ it 'responds with errors' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect_graphql_errors_to_include(/was provided invalid value for payloadAttributeMappings\.0\.type \(Expected value to not be null/)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/graphql/mutations/spammable_mutation_fields_examples.rb b/spec/support/shared_examples/graphql/mutations/spammable_mutation_fields_examples.rb
deleted file mode 100644
index 8678b23ad31..00000000000
--- a/spec/support/shared_examples/graphql/mutations/spammable_mutation_fields_examples.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.shared_examples 'spam flag is present' do
- specify :aggregate_failures do
- subject
-
- expect(mutation_response).to have_key('spam')
- expect(mutation_response['spam']).to be_falsey
- end
-end
-
-RSpec.shared_examples 'can raise spam flag' do
- it 'spam parameters are passed to the service' do
- args = [anything, anything, hash_including(api: true, request: instance_of(ActionDispatch::Request))]
- expect(service).to receive(:new).with(*args).and_call_original
-
- subject
- end
-
- context 'when the snippet is detected as spam' do
- it 'raises spam flag' do
- allow_next_instance_of(service) do |instance|
- allow(instance).to receive(:spam_check) do |snippet, user, _|
- snippet.spam!
- end
- end
-
- subject
-
- expect(mutation_response['spam']).to be true
- expect(mutation_response['errors']).to include("Your snippet has been recognized as spam and has been discarded.")
- end
- end
-
- context 'when :snippet_spam flag is disabled' do
- before do
- stub_feature_flags(snippet_spam: false)
- end
-
- it 'request parameter is not passed to the service' do
- expect(service).to receive(:new)
- .with(anything, anything, hash_not_including(request: instance_of(ActionDispatch::Request)))
- .and_call_original
-
- subject
- end
- 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 24c8a247c93..fb598b978f6 100644
--- a/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb
@@ -64,3 +64,22 @@ RSpec.shared_examples 'a Note mutation when the given resource id is not for a N
let(:match_errors) { include(/does not represent an instance of Note/) }
end
end
+
+RSpec.shared_examples 'a Note mutation when there are rate limit validation errors' do
+ before do
+ stub_application_setting(notes_create_limit: 3)
+ 3.times { post_graphql_mutation(mutation, current_user: current_user) }
+ end
+
+ it_behaves_like 'a Note mutation that does not create a Note'
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: ['This endpoint has been requested too many times. Try again later.']
+
+ context 'when the user is in the allowlist' do
+ before do
+ stub_application_setting(notes_create_limit_allowlist: ["#{current_user.username}"])
+ end
+
+ it_behaves_like 'a Note mutation that creates a Note'
+ end
+end
diff --git a/spec/support/shared_examples/helpers/issuable_description_templates_shared_examples.rb b/spec/support/shared_examples/helpers/issuable_description_templates_shared_examples.rb
new file mode 100644
index 00000000000..9e8c96d576a
--- /dev/null
+++ b/spec/support/shared_examples/helpers/issuable_description_templates_shared_examples.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'project issuable templates context' do
+ let_it_be(:issuable_template_files) do
+ {
+ '.gitlab/issue_templates/issue-bar.md' => 'Issue Template Bar',
+ '.gitlab/issue_templates/issue-foo.md' => 'Issue Template Foo',
+ '.gitlab/issue_templates/issue-bad.txt' => 'Issue Template Bad',
+ '.gitlab/issue_templates/issue-baz.xyz' => 'Issue Template Baz',
+
+ '.gitlab/merge_request_templates/merge_request-bar.md' => 'Merge Request Template Bar',
+ '.gitlab/merge_request_templates/merge_request-foo.md' => 'Merge Request Template Foo',
+ '.gitlab/merge_request_templates/merge_request-bad.txt' => 'Merge Request Template Bad',
+ '.gitlab/merge_request_templates/merge_request-baz.xyz' => 'Merge Request Template Baz'
+ }
+ end
+end
+
+RSpec.shared_examples 'project issuable templates' do
+ context 'issuable templates' do
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ end
+
+ it 'returns only md files as issue templates' do
+ expect(helper.issuable_templates(project, 'issue')).to eq(templates('issue', project))
+ end
+
+ it 'returns only md files as merge_request templates' do
+ expect(helper.issuable_templates(project, 'merge_request')).to eq(templates('merge_request', project))
+ end
+ end
+
+ def expected_templates(issuable_type)
+ expectation = {}
+
+ expectation["Project Templates"] = templates(issuable_type, project)
+ expectation["Group #{inherited_from.namespace.full_name}"] = templates(issuable_type, inherited_from) if inherited_from.present?
+
+ expectation
+ end
+
+ def templates(issuable_type, inherited_from)
+ [
+ { id: "#{issuable_type}-bar", key: "#{issuable_type}-bar", name: "#{issuable_type}-bar", project_id: inherited_from&.id },
+ { id: "#{issuable_type}-foo", key: "#{issuable_type}-foo", name: "#{issuable_type}-foo", project_id: inherited_from&.id }
+ ]
+ end
+end
diff --git a/spec/support/shared_examples/lib/api/internal_base_shared_examples.rb b/spec/support/shared_examples/lib/api/internal_base_shared_examples.rb
new file mode 100644
index 00000000000..dfa1388e0bb
--- /dev/null
+++ b/spec/support/shared_examples/lib/api/internal_base_shared_examples.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'actor key validations' do
+ context 'key id is not provided' do
+ let(:key_id) { nil }
+
+ it 'returns an error message' do
+ subject
+
+ expect(json_response['success']).to be_falsey
+ expect(json_response['message']).to eq('Could not find a user without a key')
+ end
+ end
+
+ context 'key does not exist' do
+ let(:key_id) { non_existing_record_id }
+
+ it 'returns an error message' do
+ subject
+
+ expect(json_response['success']).to be_falsey
+ expect(json_response['message']).to eq('Could not find the given key')
+ end
+ end
+
+ context 'key without user' do
+ let(:key_id) { create(:key, user: nil).id }
+
+ it 'returns an error message' do
+ subject
+
+ expect(json_response['success']).to be_falsey
+ expect(json_response['message']).to eq('Could not find a user for the given key')
+ end
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb
index 07d01d5c50e..eafb49cef71 100644
--- a/spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb
@@ -1,19 +1,35 @@
# frozen_string_literal: true
RSpec.shared_examples 'search results sorted' do
- context 'sort: newest' do
+ context 'sort: created_desc' do
let(:sort) { 'created_desc' }
it 'sorts results by created_at' do
- expect(results.objects(scope).map(&:id)).to eq([new_result.id, old_result.id, very_old_result.id])
+ expect(results_created.objects(scope).map(&:id)).to eq([new_result.id, old_result.id, very_old_result.id])
end
end
- context 'sort: oldest' do
+ context 'sort: created_asc' do
let(:sort) { 'created_asc' }
it 'sorts results by created_at' do
- expect(results.objects(scope).map(&:id)).to eq([very_old_result.id, old_result.id, new_result.id])
+ expect(results_created.objects(scope).map(&:id)).to eq([very_old_result.id, old_result.id, new_result.id])
+ end
+ end
+
+ context 'sort: updated_desc' do
+ let(:sort) { 'updated_desc' }
+
+ it 'sorts results by updated_desc' do
+ expect(results_updated.objects(scope).map(&:id)).to eq([new_updated.id, old_updated.id, very_old_updated.id])
+ end
+ end
+
+ context 'sort: updated_asc' do
+ let(:sort) { 'updated_asc' }
+
+ it 'sorts results by updated_asc' do
+ expect(results_updated.objects(scope).map(&:id)).to eq([very_old_updated.id, old_updated.id, new_updated.id])
end
end
end
diff --git a/spec/support/shared_examples/lib/gitlab/usage_data_counters/issue_activity_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/usage_data_counters/issue_activity_shared_examples.rb
index 286305f2506..edd9b6cdf37 100644
--- a/spec/support/shared_examples/lib/gitlab/usage_data_counters/issue_activity_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/usage_data_counters/issue_activity_shared_examples.rb
@@ -24,12 +24,4 @@ RSpec.shared_examples 'a tracked issue edit event' do |event|
it 'does not track edit actions if author is not present' do
expect(track_action(author: nil)).to be_nil
end
-
- context 'when feature flag track_issue_activity_actions is disabled' do
- it 'does not track edit actions' do
- stub_feature_flags(track_issue_activity_actions: false)
-
- expect(track_action(author: user1)).to be_nil
- end
- end
end
diff --git a/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb b/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb
index fe99b1cacd9..42f82987989 100644
--- a/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb
+++ b/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb
@@ -9,19 +9,30 @@ RSpec.shared_examples 'AtomicInternalId' do |validate_presence: true|
end
describe 'Validation' do
- before do
- allow_any_instance_of(described_class).to receive(:"ensure_#{scope}_#{internal_id_attribute}!")
-
- instance.valid?
- end
-
context 'when presence validation is required' do
before do
skip unless validate_presence
end
- it 'validates presence' do
- expect(instance.errors[internal_id_attribute]).to include("can't be blank")
+ context 'when creating an object' do
+ before do
+ allow_any_instance_of(described_class).to receive(:"ensure_#{scope}_#{internal_id_attribute}!")
+ end
+
+ it 'raises an error if the internal id is blank' do
+ expect { instance.save! }.to raise_error(AtomicInternalId::MissingValueError)
+ end
+ end
+
+ context 'when updating an object' do
+ it 'raises an error if the internal id is blank' do
+ instance.save!
+
+ write_internal_id(nil)
+ allow(instance).to receive(:"ensure_#{scope}_#{internal_id_attribute}!")
+
+ expect { instance.save! }.to raise_error(AtomicInternalId::MissingValueError)
+ end
end
end
@@ -30,8 +41,27 @@ RSpec.shared_examples 'AtomicInternalId' do |validate_presence: true|
skip if validate_presence
end
- it 'does not validate presence' do
- expect(instance.errors[internal_id_attribute]).to be_empty
+ context 'when creating an object' do
+ before do
+ allow_any_instance_of(described_class).to receive(:"ensure_#{scope}_#{internal_id_attribute}!")
+ end
+
+ it 'does not raise an error if the internal id is blank' do
+ expect(read_internal_id).to be_nil
+
+ expect { instance.save! }.not_to raise_error
+ end
+ end
+
+ context 'when updating an object' do
+ it 'does not raise an error if the internal id is blank' do
+ instance.save!
+
+ write_internal_id(nil)
+ allow(instance).to receive(:"ensure_#{scope}_#{internal_id_attribute}!")
+
+ expect { instance.save! }.not_to raise_error
+ end
end
end
end
@@ -76,6 +106,51 @@ RSpec.shared_examples 'AtomicInternalId' do |validate_presence: true|
end
end
+ describe 'unsetting the instance internal id on rollback' do
+ context 'when the internal id has been changed' do
+ context 'when the internal id is automatically set' do
+ it 'clears it on the instance' do
+ expect_iid_to_be_set_and_rollback
+
+ expect(read_internal_id).to be_nil
+ end
+ end
+
+ context 'when the internal id is manually set' do
+ it 'does not clear it on the instance' do
+ write_internal_id(100)
+
+ expect_iid_to_be_set_and_rollback
+
+ expect(read_internal_id).not_to be_nil
+ end
+ end
+ end
+
+ context 'when the internal id has not been changed' do
+ it 'preserves the value on the instance' do
+ instance.save!
+ original_id = read_internal_id
+
+ expect(original_id).not_to be_nil
+
+ expect_iid_to_be_set_and_rollback
+
+ expect(read_internal_id).to eq(original_id)
+ end
+ end
+
+ def expect_iid_to_be_set_and_rollback
+ ActiveRecord::Base.transaction(requires_new: true) do
+ instance.save!
+
+ expect(read_internal_id).not_to be_nil
+
+ raise ActiveRecord::Rollback
+ end
+ end
+ end
+
describe 'supply of internal ids' do
let(:scope_value) { scope_attrs.each_value.first }
let(:method_name) { :"with_#{scope}_#{internal_id_attribute}_supply" }
diff --git a/spec/support/shared_examples/models/concerns/can_housekeep_repository_shared_examples.rb b/spec/support/shared_examples/models/concerns/can_housekeep_repository_shared_examples.rb
deleted file mode 100644
index 2f0b95427d2..00000000000
--- a/spec/support/shared_examples/models/concerns/can_housekeep_repository_shared_examples.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'can housekeep repository' do
- context 'with a clean redis state', :clean_gitlab_redis_shared_state do
- describe '#pushes_since_gc' do
- context 'without any pushes' do
- it 'returns 0' do
- expect(resource.pushes_since_gc).to eq(0)
- end
- end
-
- context 'with a number of pushes' do
- it 'returns the number of pushes' do
- 3.times { resource.increment_pushes_since_gc }
-
- expect(resource.pushes_since_gc).to eq(3)
- end
- end
- end
-
- describe '#increment_pushes_since_gc' do
- it 'increments the number of pushes since the last GC' do
- 3.times { resource.increment_pushes_since_gc }
-
- expect(resource.pushes_since_gc).to eq(3)
- end
- end
-
- describe '#reset_pushes_since_gc' do
- it 'resets the number of pushes since the last GC' do
- 3.times { resource.increment_pushes_since_gc }
-
- resource.reset_pushes_since_gc
-
- expect(resource.pushes_since_gc).to eq(0)
- end
- end
-
- describe '#pushes_since_gc_redis_shared_state_key' do
- it 'returns the proper redis key format' do
- expect(resource.send(:pushes_since_gc_redis_shared_state_key)).to eq("#{resource_key}/#{resource.id}/pushes_since_gc")
- end
- end
- end
-end
diff --git a/spec/support/shared_examples/models/concerns/can_move_repository_storage_shared_examples.rb b/spec/support/shared_examples/models/concerns/can_move_repository_storage_shared_examples.rb
index 85a2c6f1449..8deeecea30d 100644
--- a/spec/support/shared_examples/models/concerns/can_move_repository_storage_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/can_move_repository_storage_shared_examples.rb
@@ -2,11 +2,12 @@
RSpec.shared_examples 'can move repository storage' do
let(:container) { raise NotImplementedError }
+ let(:repository) { container.repository }
describe '#set_repository_read_only!' do
it 'makes the repository read-only' do
expect { container.set_repository_read_only! }
- .to change(container, :repository_read_only?)
+ .to change { container.repository_read_only? }
.from(false)
.to(true)
end
@@ -28,7 +29,7 @@ RSpec.shared_examples 'can move repository storage' do
allow(container).to receive(:git_transfer_in_progress?) { true }
expect { container.set_repository_read_only!(skip_git_transfer_check: true) }
- .to change(container, :repository_read_only?)
+ .to change { container.repository_read_only? }
.from(false)
.to(true)
end
@@ -38,16 +39,16 @@ RSpec.shared_examples 'can move repository storage' do
describe '#set_repository_writable!' do
it 'sets repository_read_only to false' do
expect { container.set_repository_writable! }
- .to change(container, :repository_read_only)
+ .to change { container.repository_read_only? }
.from(true).to(false)
end
end
describe '#reference_counter' do
it 'returns a Gitlab::ReferenceCounter object' do
- expect(Gitlab::ReferenceCounter).to receive(:new).with(container.repository.gl_repository).and_call_original
+ expect(Gitlab::ReferenceCounter).to receive(:new).with(repository.gl_repository).and_call_original
- result = container.reference_counter(type: container.repository.repo_type)
+ result = container.reference_counter(type: repository.repo_type)
expect(result).to be_a Gitlab::ReferenceCounter
end
diff --git a/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb b/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb
index 826ee453919..1be4d9b80a4 100644
--- a/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb
@@ -152,36 +152,4 @@ RSpec.shared_examples 'model with repository' do
it { is_expected.to respond_to(:disk_path) }
it { is_expected.to respond_to(:gitlab_shell) }
end
-
- describe '.pick_repository_storage' do
- subject { described_class.pick_repository_storage }
-
- before do
- storages = {
- 'default' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/repositories'),
- 'picked' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/repositories')
- }
- allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
- end
-
- it 'picks storage from ApplicationSetting' do
- expect(Gitlab::CurrentSettings).to receive(:pick_repository_storage).and_return('picked')
-
- expect(subject).to eq('picked')
- end
-
- it 'picks from the available storages based on weight', :request_store do
- stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
- Gitlab::CurrentSettings.expire_current_application_settings
- Gitlab::CurrentSettings.current_application_settings
-
- settings = ApplicationSetting.last
- settings.repository_storages_weighted = { 'picked' => 100, 'default' => 0 }
- settings.save!
-
- expect(Gitlab::CurrentSettings.repository_storages_weighted).to eq({ 'default' => 100 })
- expect(subject).to eq('picked')
- expect(Gitlab::CurrentSettings.repository_storages_weighted).to eq({ 'default' => 0, 'picked' => 100 })
- end
- end
end
diff --git a/spec/support/shared_examples/models/concerns/repositories/can_housekeep_repository_shared_examples.rb b/spec/support/shared_examples/models/concerns/repositories/can_housekeep_repository_shared_examples.rb
index 2f0b95427d2..4006b8226ce 100644
--- a/spec/support/shared_examples/models/concerns/repositories/can_housekeep_repository_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/repositories/can_housekeep_repository_shared_examples.rb
@@ -41,5 +41,11 @@ RSpec.shared_examples 'can housekeep repository' do
expect(resource.send(:pushes_since_gc_redis_shared_state_key)).to eq("#{resource_key}/#{resource.id}/pushes_since_gc")
end
end
+
+ describe '#git_garbage_collect_worker_klass' do
+ it 'defines a git gargabe collect worker' do
+ expect(resource.git_garbage_collect_worker_klass).to eq(expected_worker_class)
+ end
+ end
end
end
diff --git a/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb b/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb
index 4c617f3ba46..819cf6018fe 100644
--- a/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb
@@ -33,7 +33,7 @@ RSpec.shared_examples 'handles repository moves' do
subject { build(repository_storage_factory_key, container: container) }
it "does not allow the container to be read-only on create" do
- container.update!(repository_read_only: true)
+ container.set_repository_read_only!
expect(subject).not_to be_valid
expect(subject.errors[error_key].first).to match(/is read only/)
@@ -45,8 +45,8 @@ RSpec.shared_examples 'handles repository moves' do
context 'destination_storage_name' do
subject { build(repository_storage_factory_key) }
- it 'picks storage from ApplicationSetting' do
- expect(Gitlab::CurrentSettings).to receive(:pick_repository_storage).and_return('picked').at_least(:once)
+ it 'can pick new storage' do
+ expect(Repository).to receive(:pick_storage_shard).and_return('picked').at_least(:once)
expect(subject.destination_storage_name).to eq('picked')
end
@@ -99,6 +99,11 @@ RSpec.shared_examples 'handles repository moves' do
expect(container).not_to be_repository_read_only
end
+
+ it 'updates the updated_at column of the container', :aggregate_failures do
+ expect { storage_move.finish_replication! }.to change { container.updated_at }
+ expect(storage_move.container.updated_at).to be >= storage_move.updated_at
+ end
end
context 'and transits to failed' do
diff --git a/spec/support/shared_examples/models/packages/debian/architecture_shared_examples.rb b/spec/support/shared_examples/models/packages/debian/architecture_shared_examples.rb
index 38983f752f4..b73ff516670 100644
--- a/spec/support/shared_examples/models/packages/debian/architecture_shared_examples.rb
+++ b/spec/support/shared_examples/models/packages/debian/architecture_shared_examples.rb
@@ -11,6 +11,7 @@ RSpec.shared_examples 'Debian Distribution Architecture' do |factory, container,
describe 'relationships' do
it { is_expected.to belong_to(:distribution).class_name("Packages::Debian::#{container.capitalize}Distribution").inverse_of(:architectures) }
+ it { is_expected.to have_many(:files).class_name("Packages::Debian::#{container.capitalize}ComponentFile").inverse_of(:architecture) }
end
describe 'validations' do
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
new file mode 100644
index 00000000000..02ced49ee94
--- /dev/null
+++ b/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb
@@ -0,0 +1,245 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze|
+ let_it_be(:container1, freeze: can_freeze) { create(container_type) } # rubocop:disable Rails/SaveBang
+ let_it_be(:container2, freeze: can_freeze) { create(container_type) } # rubocop:disable Rails/SaveBang
+ let_it_be(:distribution1, freeze: can_freeze) { create("debian_#{container_type}_distribution", container: container1) }
+ let_it_be(:distribution2, freeze: can_freeze) { create("debian_#{container_type}_distribution", container: container2) }
+ let_it_be(:architecture1_1, freeze: can_freeze) { create("debian_#{container_type}_architecture", distribution: distribution1) }
+ let_it_be(:architecture1_2, freeze: can_freeze) { create("debian_#{container_type}_architecture", distribution: distribution1) }
+ let_it_be(:architecture2_1, freeze: can_freeze) { create("debian_#{container_type}_architecture", distribution: distribution2) }
+ let_it_be(:architecture2_2, freeze: can_freeze) { create("debian_#{container_type}_architecture", distribution: distribution2) }
+ let_it_be(:component1_1, freeze: can_freeze) { create("debian_#{container_type}_component", distribution: distribution1) }
+ let_it_be(:component1_2, freeze: can_freeze) { create("debian_#{container_type}_component", distribution: distribution1) }
+ let_it_be(:component2_1, freeze: can_freeze) { create("debian_#{container_type}_component", distribution: distribution2) }
+ let_it_be(:component2_2, freeze: can_freeze) { create("debian_#{container_type}_component", distribution: distribution2) }
+
+ let_it_be_with_refind(:component_file_with_architecture) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1) }
+ let_it_be(:component_file_other_architecture, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_2) }
+ let_it_be(:component_file_other_component, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1_2, architecture: architecture1_1) }
+ let_it_be(:component_file_other_compression_type, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, compression_type: :xz) }
+ let_it_be(:component_file_other_file_md5, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, file_md5: 'other_md5') }
+ let_it_be(:component_file_other_file_sha256, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, file_sha256: 'other_sha256') }
+ let_it_be(:component_file_other_container, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component2_1, architecture: architecture2_1) }
+ let_it_be_with_refind(:component_file_with_file_type_source) { create("debian_#{container_type}_component_file", :source, component: component1_1) }
+ let_it_be(:component_file_with_file_type_di_packages, freeze: can_freeze) { create("debian_#{container_type}_component_file", :di_packages, component: component1_1, architecture: architecture1_1) }
+
+ subject { component_file_with_architecture }
+
+ describe 'relationships' do
+ context 'with stubbed uploader' do
+ before do
+ allow_next_instance_of(Packages::Debian::ComponentFileUploader) do |uploader|
+ allow(uploader).to receive(:dynamic_segment).and_return('stubbed')
+ end
+ end
+
+ it { is_expected.to belong_to(:component).class_name("Packages::Debian::#{container_type.capitalize}Component").inverse_of(:files) }
+ end
+
+ context 'with packages file_type' do
+ it { is_expected.to belong_to(:architecture).class_name("Packages::Debian::#{container_type.capitalize}Architecture").inverse_of(:files) }
+ end
+
+ context 'with :source file_type' do
+ subject { component_file_with_file_type_source }
+
+ it { is_expected.to belong_to(:architecture).class_name("Packages::Debian::#{container_type.capitalize}Architecture").inverse_of(:files).optional }
+ end
+ end
+
+ describe 'validations' do
+ describe "#component" do
+ before do
+ allow_next_instance_of(Packages::Debian::ComponentFileUploader) do |uploader|
+ allow(uploader).to receive(:dynamic_segment).and_return('stubbed')
+ end
+ end
+
+ it { is_expected.to validate_presence_of(:component) }
+ end
+
+ describe "#architecture" do
+ context 'with packages file_type' do
+ it { is_expected.to validate_presence_of(:architecture) }
+ end
+
+ context 'with :source file_type' do
+ subject { component_file_with_file_type_source }
+
+ it { is_expected.to validate_absence_of(:architecture) }
+ end
+ end
+
+ describe '#file_type' do
+ it { is_expected.to validate_presence_of(:file_type) }
+
+ it { is_expected.to allow_value(:packages).for(:file_type) }
+ end
+
+ describe '#compression_type' do
+ it { is_expected.not_to validate_presence_of(:compression_type) }
+
+ it { is_expected.to allow_value(nil).for(:compression_type) }
+ it { is_expected.to allow_value(:gz).for(:compression_type) }
+ end
+
+ describe '#file' do
+ subject { component_file_with_architecture.file }
+
+ context 'the uploader api' do
+ it { is_expected.to respond_to(:store_dir) }
+ it { is_expected.to respond_to(:cache_dir) }
+ it { is_expected.to respond_to(:work_dir) }
+ end
+ end
+
+ describe '#file_store' do
+ it { is_expected.to validate_presence_of(:file_store) }
+ end
+
+ describe '#file_md5' do
+ it { is_expected.to validate_presence_of(:file_md5) }
+ end
+
+ describe '#file_sha256' do
+ it { is_expected.to validate_presence_of(:file_sha256) }
+ end
+ end
+
+ describe 'scopes' do
+ describe '.with_container' do
+ subject { described_class.with_container(container2) }
+
+ it do
+ queries = ActiveRecord::QueryRecorder.new do
+ expect(subject.to_a).to contain_exactly(component_file_other_container)
+ end
+
+ expect(queries.count).to eq(1)
+ end
+ end
+
+ describe '.with_codename_or_suite' do
+ subject { described_class.with_codename_or_suite(distribution2.codename) }
+
+ it do
+ queries = ActiveRecord::QueryRecorder.new do
+ expect(subject.to_a).to contain_exactly(component_file_other_container)
+ end
+
+ expect(queries.count).to eq(1)
+ end
+ end
+
+ describe '.with_component_name' do
+ subject { described_class.with_component_name(component1_2.name) }
+
+ it do
+ queries = ActiveRecord::QueryRecorder.new do
+ expect(subject.to_a).to contain_exactly(component_file_other_component)
+ end
+
+ expect(queries.count).to eq(1)
+ end
+ end
+
+ describe '.with_file_type' do
+ subject { described_class.with_file_type(:source) }
+
+ it do
+ # let_it_be_with_refind triggers a query
+ component_file_with_file_type_source
+
+ queries = ActiveRecord::QueryRecorder.new do
+ expect(subject.to_a).to contain_exactly(component_file_with_file_type_source)
+ end
+
+ expect(queries.count).to eq(1)
+ end
+ end
+
+ describe '.with_architecture_name' do
+ subject { described_class.with_architecture_name(architecture1_2.name) }
+
+ it do
+ queries = ActiveRecord::QueryRecorder.new do
+ expect(subject.to_a).to contain_exactly(component_file_other_architecture)
+ end
+
+ expect(queries.count).to eq(1)
+ end
+ end
+
+ describe '.with_compression_type' do
+ subject { described_class.with_compression_type(:xz) }
+
+ it do
+ queries = ActiveRecord::QueryRecorder.new do
+ expect(subject.to_a).to contain_exactly(component_file_other_compression_type)
+ end
+
+ expect(queries.count).to eq(1)
+ end
+ end
+
+ describe '.with_file_sha256' do
+ subject { described_class.with_file_sha256('other_sha256') }
+
+ it do
+ queries = ActiveRecord::QueryRecorder.new do
+ expect(subject.to_a).to contain_exactly(component_file_other_file_sha256)
+ end
+
+ expect(queries.count).to eq(1)
+ end
+ end
+ end
+
+ describe 'callbacks' do
+ let(:component_file) { build("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, size: nil) }
+
+ subject { component_file.save! }
+
+ it 'updates metadata columns' do
+ expect(component_file)
+ .to receive(:update_file_store)
+ .and_call_original
+
+ expect(component_file)
+ .to receive(:update_column)
+ .with(:file_store, ::Packages::PackageFileUploader::Store::LOCAL)
+ .and_call_original
+
+ expect { subject }.to change { component_file.size }.from(nil).to(74)
+ end
+ end
+
+ describe '#relative_path' do
+ context 'with a Packages file_type' do
+ subject { component_file_with_architecture.relative_path }
+
+ it { is_expected.to eq("#{component1_1.name}/binary-#{architecture1_1.name}/Packages") }
+ end
+
+ context 'with a Source file_type' do
+ subject { component_file_with_file_type_source.relative_path }
+
+ it { is_expected.to eq("#{component1_1.name}/source/Source") }
+ end
+
+ context 'with a DI Packages file_type' do
+ subject { component_file_with_file_type_di_packages.relative_path }
+
+ it { is_expected.to eq("#{component1_1.name}/debian-installer/binary-#{architecture1_1.name}/Packages") }
+ end
+
+ context 'with an xz compression_type' do
+ subject { component_file_other_compression_type.relative_path }
+
+ it { is_expected.to eq("#{component1_1.name}/binary-#{architecture1_1.name}/Packages.xz") }
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/packages/debian/component_shared_examples.rb b/spec/support/shared_examples/models/packages/debian/component_shared_examples.rb
new file mode 100644
index 00000000000..bf6fc23116c
--- /dev/null
+++ b/spec/support/shared_examples/models/packages/debian/component_shared_examples.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples 'Debian Distribution Component' do |factory, container, can_freeze|
+ let_it_be_with_refind(:component) { create(factory) } # rubocop:disable Rails/SaveBang
+ let_it_be(:component_same_distribution, freeze: can_freeze) { create(factory, distribution: component.distribution) }
+ let_it_be(:component_same_name, freeze: can_freeze) { create(factory, name: component.name) }
+
+ subject { component }
+
+ describe 'relationships' do
+ it { is_expected.to belong_to(:distribution).class_name("Packages::Debian::#{container.capitalize}Distribution").inverse_of(:components) }
+ it { is_expected.to have_many(:files).class_name("Packages::Debian::#{container.capitalize}ComponentFile").inverse_of(:component) }
+ end
+
+ describe 'validations' do
+ describe "#distribution" do
+ it { is_expected.to validate_presence_of(:distribution) }
+ end
+
+ describe '#name' do
+ it { is_expected.to validate_presence_of(:name) }
+
+ it { is_expected.to allow_value('main').for(:name) }
+ it { is_expected.to allow_value('non-free').for(:name) }
+ it { is_expected.to allow_value('a' * 255).for(:name) }
+ it { is_expected.not_to allow_value('a' * 256).for(:name) }
+ it { is_expected.not_to allow_value('non/free').for(:name) }
+ it { is_expected.not_to allow_value('hé').for(:name) }
+ end
+ end
+
+ describe 'scopes' do
+ describe '.with_distribution' do
+ subject { described_class.with_distribution(component.distribution) }
+
+ it 'does not return other distributions' do
+ expect(subject.to_a).to eq([component, component_same_distribution])
+ end
+ end
+
+ describe '.with_name' do
+ subject { described_class.with_name(component.name) }
+
+ it 'does not return other distributions' do
+ expect(subject.to_a).to eq([component, component_same_name])
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb b/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb
index af87d30099f..b4ec146df14 100644
--- a/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb
+++ b/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb
@@ -17,7 +17,13 @@ RSpec.shared_examples 'Debian Distribution' do |factory, container, can_freeze|
it { is_expected.to belong_to(container) }
it { is_expected.to belong_to(:creator).class_name('User') }
+ it { is_expected.to have_many(:components).class_name("Packages::Debian::#{container.capitalize}Component").inverse_of(:distribution) }
it { is_expected.to have_many(:architectures).class_name("Packages::Debian::#{container.capitalize}Architecture").inverse_of(:distribution) }
+
+ if container != :group
+ it { is_expected.to have_many(:publications).class_name('Packages::Debian::Publication').inverse_of(:distribution).with_foreign_key(:distribution_id) }
+ it { is_expected.to have_many(:packages).class_name('Packages::Package').through(:publications) }
+ end
end
describe 'validations' do
diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb
index 62da9e15259..89d30688b5c 100644
--- a/spec/support/shared_examples/models/wiki_shared_examples.rb
+++ b/spec/support/shared_examples/models/wiki_shared_examples.rb
@@ -154,6 +154,15 @@ RSpec.shared_examples 'wiki model' do
it 'returns true' do
expect(subject.empty?).to be(true)
end
+
+ context 'when the repository does not exist' do
+ let(:wiki_container) { wiki_container_without_repo }
+
+ it 'returns true and does not create the repo' do
+ expect(subject.empty?).to be(true)
+ expect(wiki.repository_exists?).to be false
+ end
+ end
end
context 'when the wiki has pages' do
diff --git a/spec/support/shared_examples/models/with_debian_distributions_shared_examples.rb b/spec/support/shared_examples/models/with_debian_distributions_shared_examples.rb
new file mode 100644
index 00000000000..e86f1e77447
--- /dev/null
+++ b/spec/support/shared_examples/models/with_debian_distributions_shared_examples.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'model with Debian distributions' do
+ let(:container_type) { subject.class.name.downcase }
+ let!(:distributions) { create_list("debian_#{container_type}_distribution", 2, :with_file, container: subject) }
+ let!(:components) { create_list("debian_#{container_type}_component", 5, distribution: distributions[0]) }
+ let!(:component_files) { create_list("debian_#{container_type}_component_file", 3, component: components[0]) }
+
+ it 'removes distribution files on removal' do
+ distribution_file_paths = distributions.map do |distribution|
+ [distribution.file.path] +
+ distribution.component_files.map do |component_file|
+ component_file.file.path
+ end
+ end.flatten
+
+ expect { subject.destroy! }
+ .to change {
+ distribution_file_paths.select do |path|
+ File.exist? path
+ end.length
+ }.from(distribution_file_paths.length).to(0)
+ end
+end
diff --git a/spec/support/shared_examples/namespaces/recursive_traversal_examples.rb b/spec/support/shared_examples/namespaces/recursive_traversal_examples.rb
new file mode 100644
index 00000000000..2c94be61bc1
--- /dev/null
+++ b/spec/support/shared_examples/namespaces/recursive_traversal_examples.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'recursive namespace traversal' do
+ describe '#self_and_hierarchy' do
+ let!(:group) { create(:group, path: 'git_lab') }
+ let!(:nested_group) { create(:group, parent: group) }
+ let!(:deep_nested_group) { create(:group, parent: nested_group) }
+ let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
+ let!(:another_group) { create(:group, path: 'gitllab') }
+ let!(:another_group_nested) { create(:group, path: 'foo', parent: another_group) }
+
+ it 'returns the correct tree' do
+ expect(group.self_and_hierarchy).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group)
+ expect(nested_group.self_and_hierarchy).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group)
+ expect(very_deep_nested_group.self_and_hierarchy).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group)
+ end
+ end
+
+ describe '#ancestors' do
+ let(:group) { create(:group) }
+ let(:nested_group) { create(:group, parent: group) }
+ let(:deep_nested_group) { create(:group, parent: nested_group) }
+ let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
+
+ it 'returns the correct ancestors' do
+ expect(very_deep_nested_group.ancestors).to include(group, nested_group, deep_nested_group)
+ expect(deep_nested_group.ancestors).to include(group, nested_group)
+ expect(nested_group.ancestors).to include(group)
+ expect(group.ancestors).to eq([])
+ end
+ end
+
+ describe '#self_and_ancestors' do
+ let(:group) { create(:group) }
+ let(:nested_group) { create(:group, parent: group) }
+ let(:deep_nested_group) { create(:group, parent: nested_group) }
+ let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
+
+ it 'returns the correct ancestors' do
+ expect(very_deep_nested_group.self_and_ancestors).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group)
+ expect(deep_nested_group.self_and_ancestors).to contain_exactly(group, nested_group, deep_nested_group)
+ expect(nested_group.self_and_ancestors).to contain_exactly(group, nested_group)
+ expect(group.self_and_ancestors).to contain_exactly(group)
+ end
+ end
+
+ describe '#descendants' do
+ let!(:group) { create(:group, path: 'git_lab') }
+ let!(:nested_group) { create(:group, parent: group) }
+ let!(:deep_nested_group) { create(:group, parent: nested_group) }
+ let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
+ let!(:another_group) { create(:group, path: 'gitllab') }
+ let!(:another_group_nested) { create(:group, path: 'foo', parent: another_group) }
+
+ it 'returns the correct descendants' do
+ expect(very_deep_nested_group.descendants.to_a).to eq([])
+ expect(deep_nested_group.descendants.to_a).to include(very_deep_nested_group)
+ expect(nested_group.descendants.to_a).to include(deep_nested_group, very_deep_nested_group)
+ expect(group.descendants.to_a).to include(nested_group, deep_nested_group, very_deep_nested_group)
+ end
+ end
+
+ describe '#self_and_descendants' do
+ let!(:group) { create(:group, path: 'git_lab') }
+ let!(:nested_group) { create(:group, parent: group) }
+ let!(:deep_nested_group) { create(:group, parent: nested_group) }
+ let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
+ let!(:another_group) { create(:group, path: 'gitllab') }
+ let!(:another_group_nested) { create(:group, path: 'foo', parent: another_group) }
+
+ it 'returns the correct descendants' do
+ expect(very_deep_nested_group.self_and_descendants).to contain_exactly(very_deep_nested_group)
+ expect(deep_nested_group.self_and_descendants).to contain_exactly(deep_nested_group, very_deep_nested_group)
+ expect(nested_group.self_and_descendants).to contain_exactly(nested_group, deep_nested_group, very_deep_nested_group)
+ expect(group.self_and_descendants).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb
index e2582f20ece..4fde68efd60 100644
--- a/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb
@@ -44,7 +44,6 @@ RSpec.shared_examples 'close quick action' do |issuable_type|
it 'creates the note and interprets the close quick action accordingly' do
add_note("this is done, close\n\n/close")
- wait_for_requests
expect(page).not_to have_content '/close'
expect(page).to have_content 'this is done, close'
diff --git a/spec/support/shared_examples/quick_actions/issue/clone_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/clone_quick_action_shared_examples.rb
index a99304f7214..ab04692616a 100644
--- a/spec/support/shared_examples/quick_actions/issue/clone_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issue/clone_quick_action_shared_examples.rb
@@ -54,8 +54,6 @@ RSpec.shared_examples 'clone quick action' do
# Note that this is missing one `-`
add_note("/clone -with_notes #{target_project.full_path}")
- wait_for_requests
-
expect(page).to have_content 'Failed to clone this issue: wrong parameters.'
expect(issue.reload).to be_open
end
@@ -68,8 +66,6 @@ RSpec.shared_examples 'clone quick action' do
it 'does not clone the issue' do
add_note("/clone #{project_unauthorized.full_path}")
- wait_for_requests
-
expect(page).to have_content "Cloned this issue to #{project_unauthorized.full_path}."
expect(issue.reload).to be_open
@@ -83,8 +79,6 @@ RSpec.shared_examples 'clone quick action' do
it 'does not clone the issue' do
add_note("/clone not/valid")
- wait_for_requests
-
expect(page).to have_content "Failed to clone this issue because target project doesn't exist."
expect(issue.reload).to be_open
end
@@ -154,7 +148,6 @@ RSpec.shared_examples 'clone quick action' do
expect(issue.reload).not_to be_closed
edit_note("/cloe #{target_project.full_path}", "test note.\n/clone #{target_project.full_path}")
- wait_for_all_requests
expect(page).to have_content 'test note.'
expect(issue.reload).to be_open
@@ -172,7 +165,6 @@ RSpec.shared_examples 'clone quick action' do
expect(page).not_to have_content 'Commands applied'
edit_note("/cloe #{target_project.full_path}", "/clone #{target_project.full_path}")
- wait_for_all_requests
expect(page).not_to have_content "/clone #{target_project.full_path}"
expect(issue.reload).to be_open
diff --git a/spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb
index 910805dbdea..9dc39c6cf73 100644
--- a/spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb
@@ -22,8 +22,6 @@ RSpec.shared_examples 'create_merge_request quick action' do
branch_name = 'invalid branch name'
add_note("/create_merge_request #{branch_name}")
- wait_for_requests
-
expect_mr_quickaction(false, branch_name)
end
@@ -31,16 +29,12 @@ RSpec.shared_examples 'create_merge_request quick action' do
branch_name = 'feature'
add_note("/create_merge_request #{branch_name}")
- wait_for_requests
-
expect_mr_quickaction(false, branch_name)
end
it 'creates a new merge request using issue iid and title as branch name when the branch name is empty' do
add_note("/create_merge_request")
- wait_for_requests
-
expect_mr_quickaction(true)
created_mr = project.merge_requests.last
diff --git a/spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb
index 32c46753006..5892fc32e94 100644
--- a/spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb
@@ -27,8 +27,6 @@ RSpec.shared_examples 'move quick action' do
it 'does not move the issue' do
add_note("/move #{project_unauthorized.full_path}")
- wait_for_requests
-
expect(page).to have_content "Moved this issue to #{project_unauthorized.full_path}."
expect(issue.reload).to be_open
end
@@ -38,8 +36,6 @@ RSpec.shared_examples 'move quick action' do
it 'does not move the issue' do
add_note("/move not/valid")
- wait_for_requests
-
expect(page).to have_content "Failed to move this issue because target project doesn't exist."
expect(issue.reload).to be_open
end
@@ -110,7 +106,6 @@ RSpec.shared_examples 'move quick action' do
expect(issue.reload).not_to be_closed
edit_note("/mvoe #{target_project.full_path}", "test note.\n/move #{target_project.full_path}")
- wait_for_all_requests
expect(page).to have_content 'test note.'
expect(issue.reload).to be_closed
@@ -129,7 +124,6 @@ RSpec.shared_examples 'move quick action' do
expect(issue.reload).not_to be_closed
edit_note("/mvoe #{target_project.full_path}", "/move #{target_project.full_path}")
- wait_for_all_requests
expect(page).not_to have_content "/move #{target_project.full_path}"
expect(issue.reload).to be_closed
diff --git a/spec/support/shared_examples/quick_actions/issue/zoom_quick_actions_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/zoom_quick_actions_shared_examples.rb
index 1ea249d5f9d..34937949174 100644
--- a/spec/support/shared_examples/quick_actions/issue/zoom_quick_actions_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issue/zoom_quick_actions_shared_examples.rb
@@ -10,8 +10,6 @@ RSpec.shared_examples 'zoom quick actions' do
it 'skips addition silently' do
add_note("/zoom #{zoom_link}")
- wait_for_requests
-
expect(page).not_to have_content('Zoom meeting added')
expect(page).not_to have_content('Failed to add a Zoom meeting')
expect(ZoomMeeting.canonical_meeting_url(issue.reload)).not_to eq(zoom_link)
@@ -22,8 +20,6 @@ RSpec.shared_examples 'zoom quick actions' do
it 'adds a Zoom link' do
add_note("/zoom #{zoom_link}")
- wait_for_requests
-
expect(page).to have_content('Zoom meeting added')
expect(ZoomMeeting.canonical_meeting_url(issue.reload)).to eq(zoom_link)
end
@@ -35,8 +31,6 @@ RSpec.shared_examples 'zoom quick actions' do
it 'cannot add invalid zoom link' do
add_note("/zoom #{invalid_zoom_link}")
- wait_for_requests
-
expect(page).to have_content('Failed to add a Zoom meeting')
expect(page).not_to have_content(zoom_link)
end
@@ -64,8 +58,6 @@ RSpec.shared_examples 'zoom quick actions' do
it 'skips removal silently' do
add_note('/remove_zoom')
- wait_for_requests
-
expect(page).not_to have_content('Zoom meeting removed')
expect(page).not_to have_content('Failed to remove a Zoom meeting')
expect(ZoomMeeting.canonical_meeting_url(issue.reload)).to be_nil
@@ -78,8 +70,6 @@ RSpec.shared_examples 'zoom quick actions' do
it 'removes last Zoom link' do
add_note('/remove_zoom')
- wait_for_requests
-
expect(page).to have_content('Zoom meeting removed')
expect(ZoomMeeting.canonical_meeting_url(issue.reload)).to be_nil
end
diff --git a/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb
index 83ba72c12aa..acaa0d8c2bc 100644
--- a/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
RSpec.shared_context 'Debian repository shared context' do |object_type|
+ include_context 'workhorse headers'
+
before do
stub_feature_flags(debian_packages: true)
end
@@ -37,16 +39,15 @@ RSpec.shared_context 'Debian repository shared context' do |object_type|
let(:params) { workhorse_params }
let(:auth_headers) { {} }
- let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
- let(:workhorse_headers) do
+ let(:wh_headers) do
if method == :put
- { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token }
+ workhorse_headers
else
{}
end
end
- let(:headers) { auth_headers.merge(workhorse_headers) }
+ let(:headers) { auth_headers.merge(wh_headers) }
let(:send_rewritten_field) { true }
@@ -201,7 +202,7 @@ RSpec.shared_examples 'rejects Debian access with unknown project id' do
let(:project) { double(id: non_existing_record_id) }
context 'as anonymous' do
- it_behaves_like 'Debian project repository GET request', :anonymous, true, :not_found, nil
+ it_behaves_like 'Debian project repository GET request', :anonymous, true, :unauthorized, nil
end
context 'as authenticated user' do
@@ -228,13 +229,13 @@ RSpec.shared_examples 'Debian project repository GET endpoint' do |success_statu
'PUBLIC' | :anonymous | false | true | success_status | success_body
'PRIVATE' | :developer | true | true | success_status | success_body
'PRIVATE' | :guest | true | true | :forbidden | nil
- 'PRIVATE' | :developer | true | false | :not_found | nil
- 'PRIVATE' | :guest | true | false | :not_found | nil
+ 'PRIVATE' | :developer | true | false | :unauthorized | nil
+ 'PRIVATE' | :guest | true | false | :unauthorized | nil
'PRIVATE' | :developer | false | true | :not_found | nil
'PRIVATE' | :guest | false | true | :not_found | nil
- 'PRIVATE' | :developer | false | false | :not_found | nil
- 'PRIVATE' | :guest | false | false | :not_found | nil
- 'PRIVATE' | :anonymous | false | true | :not_found | nil
+ 'PRIVATE' | :developer | false | false | :unauthorized | nil
+ 'PRIVATE' | :guest | false | false | :unauthorized | nil
+ 'PRIVATE' | :anonymous | false | true | :unauthorized | nil
end
with_them do
@@ -263,13 +264,13 @@ RSpec.shared_examples 'Debian project repository PUT endpoint' do |success_statu
'PUBLIC' | :anonymous | false | true | :unauthorized | nil
'PRIVATE' | :developer | true | true | success_status | nil
'PRIVATE' | :guest | true | true | :forbidden | nil
- 'PRIVATE' | :developer | true | false | :not_found | nil
- 'PRIVATE' | :guest | true | false | :not_found | nil
+ 'PRIVATE' | :developer | true | false | :unauthorized | nil
+ 'PRIVATE' | :guest | true | false | :unauthorized | nil
'PRIVATE' | :developer | false | true | :not_found | nil
'PRIVATE' | :guest | false | true | :not_found | nil
- 'PRIVATE' | :developer | false | false | :not_found | nil
- 'PRIVATE' | :guest | false | false | :not_found | nil
- 'PRIVATE' | :anonymous | false | true | :not_found | nil
+ 'PRIVATE' | :developer | false | false | :unauthorized | nil
+ 'PRIVATE' | :guest | false | false | :unauthorized | nil
+ 'PRIVATE' | :anonymous | false | true | :unauthorized | nil
end
with_them do
@@ -321,7 +322,7 @@ RSpec.shared_examples 'rejects Debian access with unknown group id' do
let(:group) { double(id: non_existing_record_id) }
context 'as anonymous' do
- it_behaves_like 'Debian group repository GET request', :anonymous, true, :not_found, nil
+ it_behaves_like 'Debian group repository GET request', :anonymous, true, :unauthorized, nil
end
context 'as authenticated user' do
@@ -348,13 +349,13 @@ RSpec.shared_examples 'Debian group repository GET endpoint' do |success_status,
'PUBLIC' | :anonymous | false | true | success_status | success_body
'PRIVATE' | :developer | true | true | success_status | success_body
'PRIVATE' | :guest | true | true | :forbidden | nil
- 'PRIVATE' | :developer | true | false | :not_found | nil
- 'PRIVATE' | :guest | true | false | :not_found | nil
+ 'PRIVATE' | :developer | true | false | :unauthorized | nil
+ 'PRIVATE' | :guest | true | false | :unauthorized | nil
'PRIVATE' | :developer | false | true | :not_found | nil
'PRIVATE' | :guest | false | true | :not_found | nil
- 'PRIVATE' | :developer | false | false | :not_found | nil
- 'PRIVATE' | :guest | false | false | :not_found | nil
- 'PRIVATE' | :anonymous | false | true | :not_found | nil
+ 'PRIVATE' | :developer | false | false | :unauthorized | nil
+ 'PRIVATE' | :guest | false | false | :unauthorized | nil
+ 'PRIVATE' | :anonymous | false | true | :unauthorized | nil
end
with_them do
diff --git a/spec/support/shared_examples/requests/api/graphql/mutations/create_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/mutations/create_list_shared_examples.rb
new file mode 100644
index 00000000000..fe2cdbe3182
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/graphql/mutations/create_list_shared_examples.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples 'board lists create request' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:dev_label) do
+ create(:group_label, title: 'Development', color: '#FFAABB', group: group)
+ end
+
+ let(:mutation) { graphql_mutation(mutation_name, input) }
+ let(:mutation_response) { graphql_mutation_response(mutation_name) }
+
+ context 'the user is not allowed to read board lists' do
+ let(:input) { { board_id: board.to_global_id.to_s, backlog: true } }
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+ end
+
+ context 'when user has permissions to admin board lists' do
+ before do
+ group.add_reporter(current_user)
+ end
+
+ describe 'backlog list' do
+ let(:input) { { board_id: board.to_global_id.to_s, backlog: true } }
+
+ it 'creates the list' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['list'])
+ .to include('position' => nil, 'listType' => 'backlog')
+ end
+ end
+
+ describe 'label list' do
+ let(:input) { { board_id: board.to_global_id.to_s, label_id: dev_label.to_global_id.to_s } }
+
+ it 'creates the list' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['list'])
+ .to include('position' => 0, 'listType' => 'label', 'label' => include('title' => 'Development'))
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/graphql/noteable_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/noteable_shared_examples.rb
new file mode 100644
index 00000000000..9cf5bc04f65
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/graphql/noteable_shared_examples.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+# Requires `query(fields)`, `path_to_noteable`, `project`, and `noteable` bindings
+RSpec.shared_examples 'a noteable graphql type we can query' do
+ let(:note_factory) { :note }
+ let(:discussion_factory) { :discussion_note }
+
+ describe '.discussions' do
+ let(:fields) do
+ "discussions { nodes { #{all_graphql_fields_for('Discussion')} } }"
+ end
+
+ def expected
+ noteable.discussions.map do |discussion|
+ include(
+ 'id' => global_id_of(discussion),
+ 'replyId' => global_id_of(discussion, id: discussion.reply_id),
+ 'createdAt' => discussion.created_at.iso8601,
+ 'notes' => include(
+ 'nodes' => have_attributes(size: discussion.notes.size)
+ )
+ )
+ end
+ end
+
+ it 'can fetch discussions' do
+ create(discussion_factory, project: project, noteable: noteable)
+
+ post_graphql(query(fields), current_user: current_user)
+
+ expect(graphql_data_at(*path_to_noteable, :discussions, :nodes))
+ .to match_array(expected)
+ end
+ end
+
+ describe '.notes' do
+ let(:fields) do
+ "notes { nodes { #{all_graphql_fields_for('Note', max_depth: 2)} } }"
+ end
+
+ def expected
+ noteable.notes.map do |note|
+ include(
+ 'id' => global_id_of(note),
+ 'project' => include('id' => global_id_of(project)),
+ 'author' => include('id' => global_id_of(note.author)),
+ 'createdAt' => note.created_at.iso8601,
+ 'body' => eq(note.note)
+ )
+ end
+ end
+
+ it 'can fetch notes' do
+ create(note_factory, project: project, noteable: noteable)
+
+ post_graphql(query(fields), current_user: current_user)
+
+ expect(graphql_data_at(*path_to_noteable, :notes, :nodes))
+ .to match_array(expected)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/notes_shared_examples.rb b/spec/support/shared_examples/requests/api/notes_shared_examples.rb
index 7066f803f9d..40799688144 100644
--- a/spec/support/shared_examples/requests/api/notes_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/notes_shared_examples.rb
@@ -127,6 +127,12 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
end
describe "POST /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes" do
+ let(:params) { { body: 'hi!' } }
+
+ subject do
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: params
+ end
+
it "creates a new note" do
post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: { body: 'hi!' }
@@ -274,6 +280,29 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
expect(response).to have_gitlab_http_status(:not_found)
end
end
+
+ context 'when request exceeds the rate limit' do
+ before do
+ stub_application_setting(notes_create_limit: 1)
+ allow(::Gitlab::ApplicationRateLimiter).to receive(:increment).and_return(2)
+ end
+
+ it 'prevents user from creating more notes' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:too_many_requests)
+ expect(json_response['message']['error']).to eq('This endpoint has been requested too many times. Try again later.')
+ end
+
+ it 'allows user in allow-list to create notes' do
+ stub_application_setting(notes_create_limit_allowlist: ["#{user.username}"])
+ subject
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['body']).to eq('hi!')
+ expect(json_response['author']['username']).to eq(user.username)
+ end
+ end
end
describe "PUT /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes/:note_id" do
diff --git a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
index d3ad7aa0595..be051dcbb7b 100644
--- a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
@@ -1,270 +1,430 @@
# frozen_string_literal: true
-RSpec.shared_examples 'handling get metadata requests' do
+RSpec.shared_examples 'handling get metadata requests' do |scope: :project|
+ using RSpec::Parameterized::TableSyntax
+
let_it_be(:package_dependency_link1) { create(:packages_dependency_link, package: package, dependency_type: :dependencies) }
let_it_be(:package_dependency_link2) { create(:packages_dependency_link, package: package, dependency_type: :devDependencies) }
let_it_be(:package_dependency_link3) { create(:packages_dependency_link, package: package, dependency_type: :bundleDependencies) }
let_it_be(:package_dependency_link4) { create(:packages_dependency_link, package: package, dependency_type: :peerDependencies) }
- let(:params) { {} }
let(:headers) { {} }
- subject { get(url, params: params, headers: headers) }
+ subject { get(url, headers: headers) }
- shared_examples 'returning the npm package info' do
- it 'returns the package info' do
+ shared_examples 'accept metadata request' do |status:|
+ it 'accepts the metadata request' do
subject
- expect_a_valid_package_response
+ expect(response).to have_gitlab_http_status(status)
+ expect(response.media_type).to eq('application/json')
+ expect(response).to match_response_schema('public_api/v4/packages/npm_package')
+ expect(json_response['name']).to eq(package.name)
+ expect(json_response['versions'][package.version]).to match_schema('public_api/v4/packages/npm_package_version')
+ ::Packages::Npm::PackagePresenter::NPM_VALID_DEPENDENCY_TYPES.each do |dependency_type|
+ expect(json_response.dig('versions', package.version, dependency_type.to_s)).to be_any
+ end
+ expect(json_response['dist-tags']).to match_schema('public_api/v4/packages/npm_package_tags')
end
end
- shared_examples 'a package that requires auth' do
- it 'denies request without oauth token' do
+ shared_examples 'reject metadata request' do |status:|
+ it 'rejects the metadata request' do
subject
- expect(response).to have_gitlab_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(status)
end
+ end
- context 'with oauth token' do
- let(:params) { { access_token: token.token } }
-
- it 'returns the package info with oauth token' do
- subject
+ shared_examples 'redirect metadata request' do |status:|
+ it 'redirects metadata request' do
+ subject
- expect_a_valid_package_response
- end
+ expect(response).to have_gitlab_http_status(:found)
+ expect(response.headers['Location']).to eq("https://registry.npmjs.org/#{package_name}")
end
+ end
- context 'with job token' do
- let(:params) { { job_token: job.token } }
-
- it 'returns the package info with running job token' do
- subject
+ where(:auth, :package_name_type, :request_forward, :visibility, :user_role, :expected_result, :expected_status) do
+ nil | :scoped_naming_convention | true | 'PUBLIC' | nil | :accept | :ok
+ nil | :scoped_naming_convention | false | 'PUBLIC' | nil | :accept | :ok
+ nil | :non_existing | true | 'PUBLIC' | nil | :redirect | :redirected
+ nil | :non_existing | false | 'PUBLIC' | nil | :reject | :not_found
+ nil | :scoped_naming_convention | true | 'PRIVATE' | nil | :reject | :not_found
+ nil | :scoped_naming_convention | false | 'PRIVATE' | nil | :reject | :not_found
+ nil | :non_existing | true | 'PRIVATE' | nil | :redirect | :redirected
+ nil | :non_existing | false | 'PRIVATE' | nil | :reject | :not_found
+ nil | :scoped_naming_convention | true | 'INTERNAL' | nil | :reject | :not_found
+ nil | :scoped_naming_convention | false | 'INTERNAL' | nil | :reject | :not_found
+ nil | :non_existing | true | 'INTERNAL' | nil | :redirect | :redirected
+ nil | :non_existing | false | 'INTERNAL' | nil | :reject | :not_found
+
+ :oauth | :scoped_naming_convention | true | 'PUBLIC' | :guest | :accept | :ok
+ :oauth | :scoped_naming_convention | true | 'PUBLIC' | :reporter | :accept | :ok
+ :oauth | :scoped_naming_convention | false | 'PUBLIC' | :guest | :accept | :ok
+ :oauth | :scoped_naming_convention | false | 'PUBLIC' | :reporter | :accept | :ok
+ :oauth | :non_existing | true | 'PUBLIC' | :guest | :redirect | :redirected
+ :oauth | :non_existing | true | 'PUBLIC' | :reporter | :redirect | :redirected
+ :oauth | :non_existing | false | 'PUBLIC' | :guest | :reject | :not_found
+ :oauth | :non_existing | false | 'PUBLIC' | :reporter | :reject | :not_found
+ :oauth | :scoped_naming_convention | true | 'PRIVATE' | :guest | :reject | :forbidden
+ :oauth | :scoped_naming_convention | true | 'PRIVATE' | :reporter | :accept | :ok
+ :oauth | :scoped_naming_convention | false | 'PRIVATE' | :guest | :reject | :forbidden
+ :oauth | :scoped_naming_convention | false | 'PRIVATE' | :reporter | :accept | :ok
+ :oauth | :non_existing | true | 'PRIVATE' | :guest | :redirect | :redirected
+ :oauth | :non_existing | true | 'PRIVATE' | :reporter | :redirect | :redirected
+ :oauth | :non_existing | false | 'PRIVATE' | :guest | :reject | :forbidden
+ :oauth | :non_existing | false | 'PRIVATE' | :reporter | :reject | :not_found
+ :oauth | :scoped_naming_convention | true | 'INTERNAL' | :guest | :accept | :ok
+ :oauth | :scoped_naming_convention | true | 'INTERNAL' | :reporter | :accept | :ok
+ :oauth | :scoped_naming_convention | false | 'INTERNAL' | :guest | :accept | :ok
+ :oauth | :scoped_naming_convention | false | 'INTERNAL' | :reporter | :accept | :ok
+ :oauth | :non_existing | true | 'INTERNAL' | :guest | :redirect | :redirected
+ :oauth | :non_existing | true | 'INTERNAL' | :reporter | :redirect | :redirected
+ :oauth | :non_existing | false | 'INTERNAL' | :guest | :reject | :not_found
+ :oauth | :non_existing | false | 'INTERNAL' | :reporter | :reject | :not_found
+
+ :personal_access_token | :scoped_naming_convention | true | 'PUBLIC' | :guest | :accept | :ok
+ :personal_access_token | :scoped_naming_convention | true | 'PUBLIC' | :reporter | :accept | :ok
+ :personal_access_token | :scoped_naming_convention | false | 'PUBLIC' | :guest | :accept | :ok
+ :personal_access_token | :scoped_naming_convention | false | 'PUBLIC' | :reporter | :accept | :ok
+ :personal_access_token | :non_existing | true | 'PUBLIC' | :guest | :redirect | :redirected
+ :personal_access_token | :non_existing | true | 'PUBLIC' | :reporter | :redirect | :redirected
+ :personal_access_token | :non_existing | false | 'PUBLIC' | :guest | :reject | :not_found
+ :personal_access_token | :non_existing | false | 'PUBLIC' | :reporter | :reject | :not_found
+ :personal_access_token | :scoped_naming_convention | true | 'PRIVATE' | :guest | :reject | :forbidden
+ :personal_access_token | :scoped_naming_convention | true | 'PRIVATE' | :reporter | :accept | :ok
+ :personal_access_token | :scoped_naming_convention | false | 'PRIVATE' | :guest | :reject | :forbidden
+ :personal_access_token | :scoped_naming_convention | false | 'PRIVATE' | :reporter | :accept | :ok
+ :personal_access_token | :non_existing | true | 'PRIVATE' | :guest | :redirect | :redirected
+ :personal_access_token | :non_existing | true | 'PRIVATE' | :reporter | :redirect | :redirected
+ :personal_access_token | :non_existing | false | 'PRIVATE' | :guest | :reject | :forbidden
+ :personal_access_token | :non_existing | false | 'PRIVATE' | :reporter | :reject | :not_found
+ :personal_access_token | :scoped_naming_convention | true | 'INTERNAL' | :guest | :accept | :ok
+ :personal_access_token | :scoped_naming_convention | true | 'INTERNAL' | :reporter | :accept | :ok
+ :personal_access_token | :scoped_naming_convention | false | 'INTERNAL' | :guest | :accept | :ok
+ :personal_access_token | :scoped_naming_convention | false | 'INTERNAL' | :reporter | :accept | :ok
+ :personal_access_token | :non_existing | true | 'INTERNAL' | :guest | :redirect | :redirected
+ :personal_access_token | :non_existing | true | 'INTERNAL' | :reporter | :redirect | :redirected
+ :personal_access_token | :non_existing | false | 'INTERNAL' | :guest | :reject | :not_found
+ :personal_access_token | :non_existing | false | 'INTERNAL' | :reporter | :reject | :not_found
+
+ :job_token | :scoped_naming_convention | true | 'PUBLIC' | :developer | :accept | :ok
+ :job_token | :scoped_naming_convention | false | 'PUBLIC' | :developer | :accept | :ok
+ :job_token | :non_existing | true | 'PUBLIC' | :developer | :redirect | :redirected
+ :job_token | :non_existing | false | 'PUBLIC' | :developer | :reject | :not_found
+ :job_token | :scoped_naming_convention | true | 'PRIVATE' | :developer | :accept | :ok
+ :job_token | :scoped_naming_convention | false | 'PRIVATE' | :developer | :accept | :ok
+ :job_token | :non_existing | true | 'PRIVATE' | :developer | :redirect | :redirected
+ :job_token | :non_existing | false | 'PRIVATE' | :developer | :reject | :not_found
+ :job_token | :scoped_naming_convention | true | 'INTERNAL' | :developer | :accept | :ok
+ :job_token | :scoped_naming_convention | false | 'INTERNAL' | :developer | :accept | :ok
+ :job_token | :non_existing | true | 'INTERNAL' | :developer | :redirect | :redirected
+ :job_token | :non_existing | false | 'INTERNAL' | :developer | :reject | :not_found
+
+ :deploy_token | :scoped_naming_convention | true | 'PUBLIC' | nil | :accept | :ok
+ :deploy_token | :scoped_naming_convention | false | 'PUBLIC' | nil | :accept | :ok
+ :deploy_token | :non_existing | true | 'PUBLIC' | nil | :redirect | :redirected
+ :deploy_token | :non_existing | false | 'PUBLIC' | nil | :reject | :not_found
+ :deploy_token | :scoped_naming_convention | true | 'PRIVATE' | nil | :accept | :ok
+ :deploy_token | :scoped_naming_convention | false | 'PRIVATE' | nil | :accept | :ok
+ :deploy_token | :non_existing | true | 'PRIVATE' | nil | :redirect | :redirected
+ :deploy_token | :non_existing | false | 'PRIVATE' | nil | :reject | :not_found
+ :deploy_token | :scoped_naming_convention | true | 'INTERNAL' | nil | :accept | :ok
+ :deploy_token | :scoped_naming_convention | false | 'INTERNAL' | nil | :accept | :ok
+ :deploy_token | :non_existing | true | 'INTERNAL' | nil | :redirect | :redirected
+ :deploy_token | :non_existing | false | 'INTERNAL' | nil | :reject | :not_found
+ end
- expect_a_valid_package_response
+ with_them do
+ include_context 'set package name from package name type'
+
+ let(:headers) do
+ case auth
+ when :oauth
+ build_token_auth_header(token.token)
+ when :personal_access_token
+ build_token_auth_header(personal_access_token.token)
+ when :job_token
+ build_token_auth_header(job.token)
+ when :deploy_token
+ build_token_auth_header(deploy_token.token)
+ else
+ {}
end
+ end
- it 'denies request without running job token' do
- job.update!(status: :success)
+ before do
+ project.send("add_#{user_role}", user) if user_role
+ project.update!(visibility: Gitlab::VisibilityLevel.const_get(visibility, false))
+ package.update!(name: package_name) unless package_name == 'non-existing-package'
+ stub_application_setting(npm_package_requests_forwarding: request_forward)
+ end
- subject
+ example_name = "#{params[:expected_result]} metadata request"
+ status = params[:expected_status]
- expect(response).to have_gitlab_http_status(:unauthorized)
+ if scope == :instance && params[:package_name_type] != :scoped_naming_convention
+ if params[:request_forward]
+ example_name = 'redirect metadata request'
+ status = :redirected
+ else
+ example_name = 'reject metadata request'
+ status = :not_found
end
end
- context 'with deploy token' do
- let(:headers) { build_token_auth_header(deploy_token.token) }
+ it_behaves_like example_name, status: status
+ end
- it 'returns the package info with deploy token' do
- subject
+ context 'with a developer' do
+ let(:headers) { build_token_auth_header(personal_access_token.token) }
- expect_a_valid_package_response
- end
+ before do
+ project.add_developer(user)
end
- end
-
- context 'a public project' do
- it_behaves_like 'returning the npm package info'
context 'project path with a dot' do
before do
project.update!(path: 'foo.bar')
end
- it_behaves_like 'returning the npm package info'
+ it_behaves_like 'accept metadata request', status: :ok
end
- context 'with request forward disabled' do
+ context 'with a job token' do
+ let(:headers) { build_token_auth_header(job.token) }
+
before do
- stub_application_setting(npm_package_requests_forwarding: false)
+ job.update!(status: :success)
end
- it_behaves_like 'returning the npm package info'
+ it_behaves_like 'reject metadata request', status: :unauthorized
+ end
+ end
+end
+
+RSpec.shared_examples 'handling get dist tags requests' do |scope: :project|
+ using RSpec::Parameterized::TableSyntax
+ include_context 'set package name from package name type'
- context 'with unknown package' do
- let(:package_name) { 'unknown' }
+ let_it_be(:package_tag1) { create(:packages_tag, package: package) }
+ let_it_be(:package_tag2) { create(:packages_tag, package: package) }
- it 'returns the proper response' do
- subject
+ let(:headers) { {} }
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
+ subject { get(url, headers: headers) }
+
+ shared_examples 'reject package tags request' do |status:|
+ before do
+ package.update!(name: package_name) unless package_name == 'non-existing-package'
end
- context 'with request forward enabled' do
- before do
- stub_application_setting(npm_package_requests_forwarding: true)
- end
+ it_behaves_like 'returning response status', status
+ end
- it_behaves_like 'returning the npm package info'
+ shared_examples 'handling different package names, visibilities and user roles' do
+ where(:package_name_type, :visibility, :user_role, :expected_result, :expected_status) do
+ :scoped_naming_convention | 'PUBLIC' | :anonymous | :accept | :ok
+ :scoped_naming_convention | 'PUBLIC' | :guest | :accept | :ok
+ :scoped_naming_convention | 'PUBLIC' | :reporter | :accept | :ok
+ :non_existing | 'PUBLIC' | :anonymous | :reject | :not_found
+ :non_existing | 'PUBLIC' | :guest | :reject | :not_found
+ :non_existing | 'PUBLIC' | :reporter | :reject | :not_found
+
+ :scoped_naming_convention | 'PRIVATE' | :anonymous | :reject | :not_found
+ :scoped_naming_convention | 'PRIVATE' | :guest | :reject | :forbidden
+ :scoped_naming_convention | 'PRIVATE' | :reporter | :accept | :ok
+ :non_existing | 'PRIVATE' | :anonymous | :reject | :not_found
+ :non_existing | 'PRIVATE' | :guest | :reject | :forbidden
+ :non_existing | 'PRIVATE' | :reporter | :reject | :not_found
+
+ :scoped_naming_convention | 'INTERNAL' | :anonymous | :reject | :not_found
+ :scoped_naming_convention | 'INTERNAL' | :guest | :accept | :ok
+ :scoped_naming_convention | 'INTERNAL' | :reporter | :accept | :ok
+ :non_existing | 'INTERNAL' | :anonymous | :reject | :not_found
+ :non_existing | 'INTERNAL' | :guest | :reject | :not_found
+ :non_existing | 'INTERNAL' | :reporter | :reject | :not_found
+ end
+
+ with_them do
+ let(:anonymous) { user_role == :anonymous }
- context 'with unknown package' do
- let(:package_name) { 'unknown' }
+ subject { get(url, headers: anonymous ? {} : headers) }
- it 'returns a redirect' do
- subject
+ before do
+ project.send("add_#{user_role}", user) unless anonymous
+ project.update!(visibility: Gitlab::VisibilityLevel.const_get(visibility, false))
+ end
- expect(response).to have_gitlab_http_status(:found)
- expect(response.headers['Location']).to eq('https://registry.npmjs.org/unknown')
- end
+ example_name = "#{params[:expected_result]} package tags request"
+ status = params[:expected_status]
- it_behaves_like 'a gitlab tracking event', described_class.name, 'npm_request_forward'
+ if scope == :instance && params[:package_name_type] != :scoped_naming_convention
+ example_name = 'reject package tags request'
+ status = :not_found
end
+
+ it_behaves_like example_name, status: status
end
end
- context 'internal project' do
- before do
- project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
- end
+ context 'with oauth token' do
+ let(:headers) { build_token_auth_header(token.token) }
- it_behaves_like 'a package that requires auth'
+ it_behaves_like 'handling different package names, visibilities and user roles'
end
- context 'private project' do
- before do
- project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- end
+ context 'with personal access token' do
+ let(:headers) { build_token_auth_header(personal_access_token.token) }
- it_behaves_like 'a package that requires auth'
+ it_behaves_like 'handling different package names, visibilities and user roles'
+ end
+end
- context 'with guest' do
- let(:params) { { access_token: token.token } }
+RSpec.shared_examples 'handling create dist tag requests' do |scope: :project|
+ using RSpec::Parameterized::TableSyntax
+ include_context 'set package name from package name type'
- it 'denies request when not enough permissions' do
- project.add_guest(user)
+ let_it_be(:tag_name) { 'test' }
- subject
+ let(:params) { {} }
+ let(:version) { package.version }
+ let(:env) { { 'api.request.body': version } }
+ let(:headers) { {} }
- expect(response).to have_gitlab_http_status(:forbidden)
- end
+ shared_examples 'reject create package tag request' do |status:|
+ before do
+ package.update!(name: package_name) unless package_name == 'non-existing-package'
end
+
+ it_behaves_like 'returning response status', status
end
- def expect_a_valid_package_response
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.media_type).to eq('application/json')
- expect(response).to match_response_schema('public_api/v4/packages/npm_package')
- expect(json_response['name']).to eq(package.name)
- expect(json_response['versions'][package.version]).to match_schema('public_api/v4/packages/npm_package_version')
- ::Packages::Npm::PackagePresenter::NPM_VALID_DEPENDENCY_TYPES.each do |dependency_type|
- expect(json_response.dig('versions', package.version, dependency_type.to_s)).to be_any
+ shared_examples 'handling different package names, visibilities and user roles' do
+ where(:package_name_type, :visibility, :user_role, :expected_result, :expected_status) do
+ :scoped_naming_convention | 'PUBLIC' | :anonymous | :reject | :forbidden
+ :scoped_naming_convention | 'PUBLIC' | :guest | :reject | :forbidden
+ :scoped_naming_convention | 'PUBLIC' | :developer | :accept | :ok
+ :non_existing | 'PUBLIC' | :anonymous | :reject | :forbidden
+ :non_existing | 'PUBLIC' | :guest | :reject | :forbidden
+ :non_existing | 'PUBLIC' | :developer | :reject | :not_found
+
+ :scoped_naming_convention | 'PRIVATE' | :anonymous | :reject | :not_found
+ :scoped_naming_convention | 'PRIVATE' | :guest | :reject | :forbidden
+ :scoped_naming_convention | 'PRIVATE' | :developer | :accept | :ok
+ :non_existing | 'PRIVATE' | :anonymous | :reject | :not_found
+ :non_existing | 'PRIVATE' | :guest | :reject | :forbidden
+ :non_existing | 'PRIVATE' | :developer | :reject | :not_found
+
+ :scoped_naming_convention | 'INTERNAL' | :anonymous | :reject | :forbidden
+ :scoped_naming_convention | 'INTERNAL' | :guest | :reject | :forbidden
+ :scoped_naming_convention | 'INTERNAL' | :developer | :accept | :ok
+ :non_existing | 'INTERNAL' | :anonymous | :reject | :forbidden
+ :non_existing | 'INTERNAL' | :guest | :reject | :forbidden
+ :non_existing | 'INTERNAL' | :developer | :reject | :not_found
end
- expect(json_response['dist-tags']).to match_schema('public_api/v4/packages/npm_package_tags')
- end
-end
-RSpec.shared_examples 'handling get dist tags requests' do
- let_it_be(:package_tag1) { create(:packages_tag, package: package) }
- let_it_be(:package_tag2) { create(:packages_tag, package: package) }
+ with_them do
+ let(:anonymous) { user_role == :anonymous }
- let(:params) { {} }
+ subject { put(url, env: env, headers: headers) }
- subject { get(url, params: params) }
+ before do
+ project.send("add_#{user_role}", user) unless anonymous
+ project.update!(visibility: Gitlab::VisibilityLevel.const_get(visibility, false))
+ end
- context 'with public project' do
- context 'with authenticated user' do
- let(:params) { { private_token: personal_access_token.token } }
+ example_name = "#{params[:expected_result]} create package tag request"
+ status = params[:expected_status]
- it_behaves_like 'returns package tags', :maintainer
- it_behaves_like 'returns package tags', :developer
- it_behaves_like 'returns package tags', :reporter
- it_behaves_like 'returns package tags', :guest
- end
+ if scope == :instance && params[:package_name_type] != :scoped_naming_convention
+ example_name = 'reject create package tag request'
+ status = :not_found
+ end
- context 'with unauthenticated user' do
- it_behaves_like 'returns package tags', :no_type
+ it_behaves_like example_name, status: status
end
end
- context 'with private project' do
- before do
- project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- end
+ context 'with oauth token' do
+ let(:headers) { build_token_auth_header(token.token) }
- context 'with authenticated user' do
- let(:params) { { private_token: personal_access_token.token } }
+ it_behaves_like 'handling different package names, visibilities and user roles'
+ end
- it_behaves_like 'returns package tags', :maintainer
- it_behaves_like 'returns package tags', :developer
- it_behaves_like 'returns package tags', :reporter
- it_behaves_like 'rejects package tags access', :guest, :forbidden
- end
+ context 'with personal access token' do
+ let(:headers) { build_token_auth_header(personal_access_token.token) }
- context 'with unauthenticated user' do
- it_behaves_like 'rejects package tags access', :no_type, :not_found
- end
+ it_behaves_like 'handling different package names, visibilities and user roles'
end
end
-RSpec.shared_examples 'handling create dist tag requests' do
- let_it_be(:tag_name) { 'test' }
+RSpec.shared_examples 'handling delete dist tag requests' do |scope: :project|
+ using RSpec::Parameterized::TableSyntax
+ include_context 'set package name from package name type'
- let(:params) { {} }
- let(:env) { {} }
- let(:version) { package.version }
-
- subject { put(url, env: env, params: params) }
+ let_it_be(:package_tag) { create(:packages_tag, package: package) }
- context 'with public project' do
- context 'with authenticated user' do
- let(:params) { { private_token: personal_access_token.token } }
- let(:env) { { 'api.request.body': version } }
+ let(:tag_name) { package_tag.name }
+ let(:headers) { {} }
- it_behaves_like 'create package tag', :maintainer
- it_behaves_like 'create package tag', :developer
- it_behaves_like 'rejects package tags access', :reporter, :forbidden
- it_behaves_like 'rejects package tags access', :guest, :forbidden
+ shared_examples 'reject delete package tag request' do |status:|
+ before do
+ package.update!(name: package_name) unless package_name == 'non-existing-package'
end
- context 'with unauthenticated user' do
- it_behaves_like 'rejects package tags access', :no_type, :unauthorized
- end
+ it_behaves_like 'returning response status', status
end
-end
-RSpec.shared_examples 'handling delete dist tag requests' do
- let_it_be(:package_tag) { create(:packages_tag, package: package) }
+ shared_examples 'handling different package names, visibilities and user roles' do
+ where(:package_name_type, :visibility, :user_role, :expected_result, :expected_status) do
+ :scoped_naming_convention | 'PUBLIC' | :anonymous | :reject | :forbidden
+ :scoped_naming_convention | 'PUBLIC' | :guest | :reject | :forbidden
+ :scoped_naming_convention | 'PUBLIC' | :maintainer | :accept | :ok
+ :non_existing | 'PUBLIC' | :anonymous | :reject | :forbidden
+ :non_existing | 'PUBLIC' | :guest | :reject | :forbidden
+ :non_existing | 'PUBLIC' | :maintainer | :reject | :not_found
+
+ :scoped_naming_convention | 'PRIVATE' | :anonymous | :reject | :not_found
+ :scoped_naming_convention | 'PRIVATE' | :guest | :reject | :forbidden
+ :scoped_naming_convention | 'PRIVATE' | :maintainer | :accept | :ok
+ :non_existing | 'INTERNAL' | :anonymous | :reject | :forbidden
+ :non_existing | 'INTERNAL' | :guest | :reject | :forbidden
+ :non_existing | 'INTERNAL' | :maintainer | :reject | :not_found
+ end
- let(:params) { {} }
- let(:tag_name) { package_tag.name }
+ with_them do
+ let(:anonymous) { user_role == :anonymous }
+
+ subject { delete(url, headers: headers) }
- subject { delete(url, params: params) }
+ before do
+ project.send("add_#{user_role}", user) unless anonymous
+ project.update!(visibility: Gitlab::VisibilityLevel.const_get(visibility, false))
+ end
- context 'with public project' do
- context 'with authenticated user' do
- let(:params) { { private_token: personal_access_token.token } }
+ example_name = "#{params[:expected_result]} delete package tag request"
+ status = params[:expected_status]
- it_behaves_like 'delete package tag', :maintainer
- it_behaves_like 'rejects package tags access', :developer, :forbidden
- it_behaves_like 'rejects package tags access', :reporter, :forbidden
- it_behaves_like 'rejects package tags access', :guest, :forbidden
- end
+ if scope == :instance && params[:package_name_type] != :scoped_naming_convention
+ example_name = 'reject delete package tag request'
+ status = :not_found
+ end
- context 'with unauthenticated user' do
- it_behaves_like 'rejects package tags access', :no_type, :unauthorized
+ it_behaves_like example_name, status: status
end
end
- context 'with private project' do
- before do
- project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- end
+ context 'with oauth token' do
+ let(:headers) { build_token_auth_header(token.token) }
- context 'with authenticated user' do
- let(:params) { { private_token: personal_access_token.token } }
+ it_behaves_like 'handling different package names, visibilities and user roles'
+ end
- it_behaves_like 'delete package tag', :maintainer
- it_behaves_like 'rejects package tags access', :developer, :forbidden
- it_behaves_like 'rejects package tags access', :reporter, :forbidden
- it_behaves_like 'rejects package tags access', :guest, :forbidden
- end
+ context 'with personal access token' do
+ let(:headers) { build_token_auth_header(personal_access_token.token) }
- context 'with unauthenticated user' do
- it_behaves_like 'rejects package tags access', :no_type, :unauthorized
- end
+ it_behaves_like 'handling different package names, visibilities and user roles'
end
end
diff --git a/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb b/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb
new file mode 100644
index 00000000000..e6b3dc74b74
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb
@@ -0,0 +1,190 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'rejects package tags access' do |status:|
+ before do
+ package.update!(name: package_name) unless package_name == 'non-existing-package'
+ end
+
+ it_behaves_like 'returning response status', status
+end
+
+RSpec.shared_examples 'accept package tags request' do |status:|
+ using RSpec::Parameterized::TableSyntax
+
+ before do
+ stub_application_setting(npm_package_requests_forwarding: false)
+ end
+
+ context 'with valid package name' do
+ before do
+ package.update!(name: package_name) unless package_name == 'non-existing-package'
+ end
+
+ it_behaves_like 'returning response status', status
+
+ it 'returns a valid json response' do
+ subject
+
+ expect(response.media_type).to eq('application/json')
+ expect(json_response).to be_a(Hash)
+ end
+
+ it 'returns two package tags' do
+ subject
+
+ expect(json_response).to match_schema('public_api/v4/packages/npm_package_tags')
+ expect(json_response.length).to eq(3) # two tags + latest (auto added)
+ expect(json_response[package_tag1.name]).to eq(package.version)
+ expect(json_response[package_tag2.name]).to eq(package.version)
+ expect(json_response['latest']).to eq(package.version)
+ end
+ end
+
+ context 'with invalid package name' do
+ where(:package_name, :status) do
+ '%20' | :bad_request
+ nil | :not_found
+ end
+
+ with_them do
+ it_behaves_like 'returning response status', params[:status]
+ end
+ end
+end
+
+RSpec.shared_examples 'accept create package tag request' do |user_type|
+ using RSpec::Parameterized::TableSyntax
+
+ context 'with valid package name' do
+ before do
+ package.update!(name: package_name) unless package_name == 'non-existing-package'
+ end
+
+ it_behaves_like 'returning response status', :no_content
+
+ it 'creates the package tag' do
+ expect { subject }.to change { Packages::Tag.count }.by(1)
+
+ last_tag = Packages::Tag.last
+ expect(last_tag.name).to eq(tag_name)
+ expect(last_tag.package).to eq(package)
+ end
+
+ it 'returns a valid response' do
+ subject
+
+ expect(response.body).to be_empty
+ end
+
+ context 'with already existing tag' do
+ let_it_be(:package2) { create(:npm_package, project: project, name: package.name, version: '5.5.55') }
+ let_it_be(:tag) { create(:packages_tag, package: package2, name: tag_name) }
+
+ it_behaves_like 'returning response status', :no_content
+
+ it 'reuses existing tag' do
+ expect(package.tags).to be_empty
+ expect(package2.tags).to eq([tag])
+ expect { subject }.to not_change { Packages::Tag.count }
+ expect(package.reload.tags).to eq([tag])
+ expect(package2.reload.tags).to be_empty
+ end
+
+ it 'returns a valid response' do
+ subject
+
+ expect(response.body).to be_empty
+ end
+ end
+ end
+
+ context 'with invalid package name' do
+ where(:package_name, :status) do
+ 'unknown' | :not_found
+ '' | :not_found
+ '%20' | :bad_request
+ end
+
+ with_them do
+ it_behaves_like 'returning response status', params[:status]
+ end
+ end
+
+ context 'with invalid tag name' do
+ where(:tag_name, :status) do
+ '' | :not_found
+ '%20' | :bad_request
+ end
+
+ with_them do
+ it_behaves_like 'returning response status', params[:status]
+ end
+ end
+
+ context 'with invalid version' do
+ where(:version, :status) do
+ ' ' | :bad_request
+ '' | :bad_request
+ nil | :bad_request
+ end
+
+ with_them do
+ it_behaves_like 'returning response status', params[:status]
+ end
+ end
+end
+
+RSpec.shared_examples 'accept delete package tag request' do |user_type|
+ using RSpec::Parameterized::TableSyntax
+
+ context 'with valid package name' do
+ before do
+ package.update!(name: package_name) unless package_name == 'non-existing-package'
+ end
+
+ it_behaves_like 'returning response status', :no_content
+
+ it 'returns a valid response' do
+ subject
+
+ expect(response.body).to be_empty
+ end
+
+ it 'destroy the package tag' do
+ expect(package.tags).to eq([package_tag])
+ expect { subject }.to change { Packages::Tag.count }.by(-1)
+ expect(package.reload.tags).to be_empty
+ end
+
+ context 'with tag from other package' do
+ let(:package2) { create(:npm_package, project: project) }
+ let(:package_tag) { create(:packages_tag, package: package2) }
+
+ it_behaves_like 'returning response status', :not_found
+ end
+ end
+
+ context 'with invalid package name' do
+ where(:package_name, :status) do
+ 'unknown' | :not_found
+ '' | :not_found
+ '%20' | :bad_request
+ end
+
+ with_them do
+ it_behaves_like 'returning response status', params[:status]
+ end
+ end
+
+ context 'with invalid tag name' do
+ where(:tag_name, :status) do
+ 'unknown' | :not_found
+ '' | :not_found
+ '%20' | :bad_request
+ end
+
+ with_them do
+ it_behaves_like 'returning response status', params[:status]
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb
index 8b60857cdaf..617fdecbb5b 100644
--- a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb
@@ -123,7 +123,7 @@ RSpec.shared_examples 'process nuget workhorse authorization' do |user_type, sta
context 'with a request that bypassed gitlab-workhorse' do
let(:headers) do
basic_auth_header(user.username, personal_access_token.token)
- .merge(workhorse_header)
+ .merge(workhorse_headers)
.tap { |h| h.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER) }
end
diff --git a/spec/support/shared_examples/requests/api/packages_shared_examples.rb b/spec/support/shared_examples/requests/api/packages_shared_examples.rb
index 3833604e304..15976eed021 100644
--- a/spec/support/shared_examples/requests/api/packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/packages_shared_examples.rb
@@ -24,7 +24,7 @@ end
RSpec.shared_examples 'deploy token for package uploads' do
context 'with deploy token headers' do
- let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_header) }
+ let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_headers) }
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
@@ -35,7 +35,7 @@ RSpec.shared_examples 'deploy token for package uploads' do
end
context 'invalid token' do
- let(:headers) { basic_auth_header(deploy_token.username, 'bar').merge(workhorse_header) }
+ let(:headers) { basic_auth_header(deploy_token.username, 'bar').merge(workhorse_headers) }
it_behaves_like 'returning response status', :unauthorized
end
@@ -102,7 +102,7 @@ end
RSpec.shared_examples 'job token for package uploads' do
context 'with job token headers' do
- let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, job.token).merge(workhorse_header) }
+ let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, job.token).merge(workhorse_headers) }
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
@@ -114,13 +114,13 @@ RSpec.shared_examples 'job token for package uploads' do
end
context 'invalid token' do
- let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, 'bar').merge(workhorse_header) }
+ let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, 'bar').merge(workhorse_headers) }
it_behaves_like 'returning response status', :unauthorized
end
context 'invalid user' do
- let(:headers) { basic_auth_header('foo', job.token).merge(workhorse_header) }
+ let(:headers) { basic_auth_header('foo', job.token).merge(workhorse_headers) }
it_behaves_like 'returning response status', :unauthorized
end
diff --git a/spec/support/shared_examples/requests/api/packages_tags_shared_examples.rb b/spec/support/shared_examples/requests/api/packages_tags_shared_examples.rb
deleted file mode 100644
index 2c203dc096e..00000000000
--- a/spec/support/shared_examples/requests/api/packages_tags_shared_examples.rb
+++ /dev/null
@@ -1,185 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'rejects package tags access' do |user_type, status|
- context "for user type #{user_type}" do
- before do
- project.send("add_#{user_type}", user) unless user_type == :no_type
- end
-
- it_behaves_like 'returning response status', status
- end
-end
-
-RSpec.shared_examples 'returns package tags' do |user_type|
- using RSpec::Parameterized::TableSyntax
-
- before do
- stub_application_setting(npm_package_requests_forwarding: false)
- project.send("add_#{user_type}", user) unless user_type == :no_type
- end
-
- it_behaves_like 'returning response status', :success
-
- it 'returns a valid json response' do
- subject
-
- expect(response.media_type).to eq('application/json')
- expect(json_response).to be_a(Hash)
- end
-
- it 'returns two package tags' do
- subject
-
- expect(json_response).to match_schema('public_api/v4/packages/npm_package_tags')
- expect(json_response.length).to eq(3) # two tags + latest (auto added)
- expect(json_response[package_tag1.name]).to eq(package.version)
- expect(json_response[package_tag2.name]).to eq(package.version)
- expect(json_response['latest']).to eq(package.version)
- end
-
- context 'with invalid package name' do
- where(:package_name, :status) do
- '%20' | :bad_request
- nil | :not_found
- end
-
- with_them do
- it_behaves_like 'returning response status', params[:status]
- end
- end
-end
-
-RSpec.shared_examples 'create package tag' do |user_type|
- using RSpec::Parameterized::TableSyntax
-
- before do
- project.send("add_#{user_type}", user) unless user_type == :no_type
- end
-
- it_behaves_like 'returning response status', :no_content
-
- it 'creates the package tag' do
- expect { subject }.to change { Packages::Tag.count }.by(1)
-
- last_tag = Packages::Tag.last
- expect(last_tag.name).to eq(tag_name)
- expect(last_tag.package).to eq(package)
- end
-
- it 'returns a valid response' do
- subject
-
- expect(response.body).to be_empty
- end
-
- context 'with already existing tag' do
- let_it_be(:package2) { create(:npm_package, project: project, name: package.name, version: '5.5.55') }
- let_it_be(:tag) { create(:packages_tag, package: package2, name: tag_name) }
-
- it_behaves_like 'returning response status', :no_content
-
- it 'reuses existing tag' do
- expect(package.tags).to be_empty
- expect(package2.tags).to eq([tag])
- expect { subject }.to not_change { Packages::Tag.count }
- expect(package.reload.tags).to eq([tag])
- expect(package2.reload.tags).to be_empty
- end
-
- it 'returns a valid response' do
- subject
-
- expect(response.body).to be_empty
- end
- end
-
- context 'with invalid package name' do
- where(:package_name, :status) do
- 'unknown' | :not_found
- '' | :not_found
- '%20' | :bad_request
- end
-
- with_them do
- it_behaves_like 'returning response status', params[:status]
- end
- end
-
- context 'with invalid tag name' do
- where(:tag_name, :status) do
- '' | :not_found
- '%20' | :bad_request
- end
-
- with_them do
- it_behaves_like 'returning response status', params[:status]
- end
- end
-
- context 'with invalid version' do
- where(:version, :status) do
- ' ' | :bad_request
- '' | :bad_request
- nil | :bad_request
- end
-
- with_them do
- it_behaves_like 'returning response status', params[:status]
- end
- end
-end
-
-RSpec.shared_examples 'delete package tag' do |user_type|
- using RSpec::Parameterized::TableSyntax
-
- before do
- project.send("add_#{user_type}", user) unless user_type == :no_type
- end
-
- context "for #{user_type} user" do
- it_behaves_like 'returning response status', :no_content
-
- it 'returns a valid response' do
- subject
-
- expect(response.body).to be_empty
- end
-
- it 'destroy the package tag' do
- expect(package.tags).to eq([package_tag])
- expect { subject }.to change { Packages::Tag.count }.by(-1)
- expect(package.reload.tags).to be_empty
- end
-
- context 'with tag from other package' do
- let(:package2) { create(:npm_package, project: project) }
- let(:package_tag) { create(:packages_tag, package: package2) }
-
- it_behaves_like 'returning response status', :not_found
- end
-
- context 'with invalid package name' do
- where(:package_name, :status) do
- 'unknown' | :not_found
- '' | :not_found
- '%20' | :bad_request
- end
-
- with_them do
- it_behaves_like 'returning response status', params[:status]
- end
- end
-
- context 'with invalid tag name' do
- where(:tag_name, :status) do
- 'unknown' | :not_found
- '' | :not_found
- '%20' | :bad_request
- end
-
- with_them do
- it_behaves_like 'returning response status', params[:status]
- end
- end
- end
-end
diff --git a/spec/support/shared_examples/requests/api/read_user_shared_examples.rb b/spec/support/shared_examples/requests/api/read_user_shared_examples.rb
index 59cd0ab67b4..b9fd997bd2c 100644
--- a/spec/support/shared_examples/requests/api/read_user_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/read_user_shared_examples.rb
@@ -7,21 +7,33 @@ RSpec.shared_examples 'allows the "read_user" scope' do |api_version|
context 'when the requesting token has the "api" scope' do
let(:token) { create(:personal_access_token, scopes: ['api'], user: user) }
- it 'returns a "200" response' do
+ it 'returns a "200" response on get request' do
get api_call.call(path, user, personal_access_token: token, version: version)
expect(response).to have_gitlab_http_status(:ok)
end
+
+ it 'returns a "200" response on head request' do
+ head api_call.call(path, user, personal_access_token: token, version: version)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
context 'when the requesting token has the "read_user" scope' do
let(:token) { create(:personal_access_token, scopes: ['read_user'], user: user) }
- it 'returns a "200" response' do
+ it 'returns a "200" response on get request' do
get api_call.call(path, user, personal_access_token: token, version: version)
expect(response).to have_gitlab_http_status(:ok)
end
+
+ it 'returns a "200" response on head request' do
+ head api_call.call(path, user, personal_access_token: token, version: version)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
context 'when the requesting token does not have any required scope' do
@@ -45,21 +57,33 @@ RSpec.shared_examples 'allows the "read_user" scope' do |api_version|
context 'when the requesting token has the "api" scope' do
let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "api" }
- it 'returns a "200" response' do
+ it 'returns a "200" response on get request' do
get api_call.call(path, user, oauth_access_token: token)
expect(response).to have_gitlab_http_status(:ok)
end
+
+ it 'returns a "200" response on head request' do
+ head api_call.call(path, user, oauth_access_token: token)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
context 'when the requesting token has the "read_user" scope' do
let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "read_user" }
- it 'returns a "200" response' do
+ it 'returns a "200" response on get request' do
get api_call.call(path, user, oauth_access_token: token)
expect(response).to have_gitlab_http_status(:ok)
end
+
+ it 'returns a "200" response on head request' do
+ head api_call.call(path, user, oauth_access_token: token)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
context 'when the requesting token does not have any required scope' do
diff --git a/spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb b/spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb
index b2970fd265d..3ca2b9fa6de 100644
--- a/spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb
@@ -85,14 +85,37 @@ RSpec.shared_examples 'repository_storage_moves API' do |container_type|
end
describe "GET /#{container_type}/:id/repository_storage_moves" do
- it_behaves_like 'get container repository storage move list' do
- let(:url) { "/#{container_type}/#{container.id}/repository_storage_moves" }
+ let(:container_id) { container.id }
+ let(:url) { "/#{container_type}/#{container_id}/repository_storage_moves" }
+
+ it_behaves_like 'get container repository storage move list'
+
+ context 'non-existent container' do
+ let(:container_id) { non_existing_record_id }
+
+ it 'returns not found' do
+ get api(url, user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
end
end
describe "GET /#{container_type}/:id/repository_storage_moves/:repository_storage_move_id" do
- it_behaves_like 'get single container repository storage move' do
- let(:url) { "/#{container_type}/#{container.id}/repository_storage_moves/#{repository_storage_move_id}" }
+ let(:container_id) { container.id }
+ let(:url) { "/#{container_type}/#{container_id}/repository_storage_moves/#{repository_storage_move_id}" }
+
+ it_behaves_like 'get single container repository storage move'
+
+ context 'non-existent container' do
+ let(:container_id) { non_existing_record_id }
+ let(:repository_storage_move_id) { storage_move.id }
+
+ it 'returns not found' do
+ get api(url, user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
end
end
@@ -109,7 +132,8 @@ RSpec.shared_examples 'repository_storage_moves API' do |container_type|
end
describe "POST /#{container_type}/:id/repository_storage_moves" do
- let(:url) { "/#{container_type}/#{container.id}/repository_storage_moves" }
+ let(:container_id) { container.id }
+ let(:url) { "/#{container_type}/#{container_id}/repository_storage_moves" }
let(:destination_storage_name) { 'test_second_storage' }
def create_container_repository_storage_move
@@ -154,6 +178,16 @@ RSpec.shared_examples 'repository_storage_moves API' do |container_type|
expect(json_response['destination_storage_name']).to be_present
end
end
+
+ context 'when container does not exist' do
+ let(:container_id) { non_existing_record_id }
+
+ it 'returns not found' do
+ create_container_repository_storage_move
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
end
describe "POST /#{container_type.singularize}_repository_storage_moves" do
diff --git a/spec/support/shared_examples/requests/api/resolvable_discussions_shared_examples.rb b/spec/support/shared_examples/requests/api/resolvable_discussions_shared_examples.rb
index 460e8d57a2b..b5139bd8c99 100644
--- a/spec/support/shared_examples/requests/api/resolvable_discussions_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/resolvable_discussions_shared_examples.rb
@@ -13,6 +13,9 @@ RSpec.shared_examples 'resolvable discussions API' do |parent_type, noteable_typ
end
it "unresolves discussion if resolved is false" do
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to receive(:track_unresolve_thread_action).with(user: user)
+
put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
"discussions/#{note.discussion_id}", user), params: { resolved: false }
diff --git a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
index 3b039049ca9..926da827e75 100644
--- a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
+++ b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
@@ -112,7 +112,7 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do
expect(response).not_to have_gitlab_http_status(:too_many_requests)
end
- arguments = {
+ arguments = a_hash_including({
message: 'Rack_Attack',
env: :throttle,
remote_ip: '127.0.0.1',
@@ -121,7 +121,7 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do
user_id: user.id,
'meta.user' => user.username,
matched: throttle_types[throttle_setting_prefix]
- }
+ })
expect(Gitlab::AuthLogger).to receive(:error).with(arguments).once
@@ -278,7 +278,7 @@ RSpec.shared_examples 'rate-limited web authenticated requests' do
expect(response).not_to have_gitlab_http_status(:too_many_requests)
end
- arguments = {
+ arguments = a_hash_including({
message: 'Rack_Attack',
env: :throttle,
remote_ip: '127.0.0.1',
@@ -287,7 +287,7 @@ RSpec.shared_examples 'rate-limited web authenticated requests' do
user_id: user.id,
'meta.user' => user.username,
matched: throttle_types[throttle_setting_prefix]
- }
+ })
expect(Gitlab::AuthLogger).to receive(:error).with(arguments).once
expect { request_authenticated_web_url }.not_to exceed_query_limit(control_count)
diff --git a/spec/support/shared_examples/services/boards/boards_list_service_shared_examples.rb b/spec/support/shared_examples/services/boards/boards_list_service_shared_examples.rb
index 8f7c08ed625..0e2bddc19ab 100644
--- a/spec/support/shared_examples/services/boards/boards_list_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/boards/boards_list_service_shared_examples.rb
@@ -1,32 +1,8 @@
# frozen_string_literal: true
RSpec.shared_examples 'boards list service' do
- context 'when parent does not have a board' do
- it 'creates a new parent board' do
- expect { service.execute }.to change(parent.boards, :count).by(1)
- end
-
- it 'delegates the parent board creation to Boards::CreateService' do
- expect_any_instance_of(Boards::CreateService).to receive(:execute).once
-
- service.execute
- end
-
- context 'when create_default_board is false' do
- it 'does not create a new parent board' do
- expect { service.execute(create_default_board: false) }.not_to change(parent.boards, :count)
- end
- end
- end
-
- context 'when parent has a board' do
- before do
- create(:board, resource_parent: parent)
- end
-
- it 'does not create a new board' do
- expect { service.execute }.not_to change(parent.boards, :count)
- end
+ it 'does not create a new board' do
+ expect { service.execute }.not_to change(parent.boards, :count)
end
it 'returns parent boards' do
diff --git a/spec/support/shared_examples/services/boards/lists_create_service_shared_examples.rb b/spec/support/shared_examples/services/boards/lists_create_service_shared_examples.rb
new file mode 100644
index 00000000000..3be002c2126
--- /dev/null
+++ b/spec/support/shared_examples/services/boards/lists_create_service_shared_examples.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'board lists create service' do
+ describe '#execute' do
+ let_it_be(:user) { create(:user) }
+
+ before_all do
+ parent.add_developer(user)
+ end
+
+ subject(:service) { described_class.new(parent, user, label_id: label.id) }
+
+ context 'when board lists is empty' do
+ it 'creates a new list at beginning of the list' do
+ response = service.execute(board)
+
+ expect(response.success?).to eq(true)
+ expect(response.payload[:list].position).to eq 0
+ end
+ end
+
+ context 'when board lists has the done list' do
+ it 'creates a new list at beginning of the list' do
+ response = service.execute(board)
+
+ expect(response.success?).to eq(true)
+ expect(response.payload[:list].position).to eq 0
+ end
+ end
+
+ context 'when board lists has labels lists' do
+ it 'creates a new list at end of the lists' do
+ create_list(position: 0)
+ create_list(position: 1)
+
+ response = service.execute(board)
+
+ expect(response.success?).to eq(true)
+ expect(response.payload[:list].position).to eq 2
+ end
+ end
+
+ context 'when board lists has label and done lists' do
+ it 'creates a new list at end of the label lists' do
+ list1 = create_list(position: 0)
+
+ list2 = service.execute(board).payload[:list]
+
+ expect(list1.reload.position).to eq 0
+ expect(list2.reload.position).to eq 1
+ end
+ end
+
+ context 'when provided label does not belong to the parent' do
+ it 'returns an error' do
+ label = create(:label, name: 'in-development')
+ service = described_class.new(parent, user, label_id: label.id)
+
+ response = service.execute(board)
+
+ expect(response.success?).to eq(false)
+ expect(response.errors).to include('Label not found')
+ end
+ end
+
+ context 'when backlog param is sent' do
+ it 'creates one and only one backlog list' do
+ service = described_class.new(parent, user, 'backlog' => true)
+ list = service.execute(board).payload[:list]
+
+ expect(list.list_type).to eq('backlog')
+ expect(list.position).to be_nil
+ expect(list).to be_valid
+
+ another_backlog = service.execute(board).payload[:list]
+
+ expect(another_backlog).to eq list
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/issuable_shared_examples.rb b/spec/support/shared_examples/services/issuable_shared_examples.rb
index 47c7a1e7356..5b3e0f9e0b9 100644
--- a/spec/support/shared_examples/services/issuable_shared_examples.rb
+++ b/spec/support/shared_examples/services/issuable_shared_examples.rb
@@ -18,6 +18,27 @@ RSpec.shared_examples 'updating a single task' do
update_issuable(description: "- [ ] Task 1\n- [ ] Task 2")
end
+ context 'usage counters' do
+ it 'update as expected' do
+ if try(:merge_request)
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to receive(:track_task_item_status_changed).once.with(user: user)
+ else
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .not_to receive(:track_task_item_status_changed)
+ end
+
+ update_issuable(
+ update_task: {
+ index: 1,
+ checked: true,
+ line_source: '- [ ] Task 1',
+ line_number: 1
+ }
+ )
+ end
+ end
+
context 'when a task is marked as completed' do
before do
update_issuable(update_task: { index: 1, checked: true, line_source: '- [ ] Task 1', line_number: 1 })
diff --git a/spec/support/shared_examples/services/packages_shared_examples.rb b/spec/support/shared_examples/services/packages_shared_examples.rb
index fa307d2a9a6..4e34c191306 100644
--- a/spec/support/shared_examples/services/packages_shared_examples.rb
+++ b/spec/support/shared_examples/services/packages_shared_examples.rb
@@ -40,6 +40,19 @@ RSpec.shared_examples 'assigns the package creator' do
end
end
+RSpec.shared_examples 'assigns status to package' do
+ context 'with status param' do
+ let_it_be(:status) { 'hidden' }
+ let(:params) { super().merge(status: status) }
+
+ it 'assigns the status to the package' do
+ package = subject
+
+ expect(package.status).to eq(status)
+ end
+ end
+end
+
RSpec.shared_examples 'returns packages' do |container_type, user_type|
context "for #{user_type}" do
before do
@@ -190,6 +203,7 @@ RSpec.shared_examples 'filters on each package_type' do |is_project: false|
let_it_be(:package7) { create(:generic_package, project: project) }
let_it_be(:package8) { create(:golang_package, project: project) }
let_it_be(:package9) { create(:debian_package, project: project) }
+ let_it_be(:package9) { create(:rubygems_package, project: project) }
Packages::Package.package_types.keys.each do |package_type|
context "for package type #{package_type}" do
@@ -262,3 +276,41 @@ RSpec.shared_examples 'with versionless packages' do
end
end
end
+
+RSpec.shared_examples 'with status param' do
+ context 'hidden packages' do
+ let!(:hidden_package) { create(:maven_package, :hidden, project: project) }
+
+ shared_examples 'not including the hidden package' do
+ it 'does not return the package' do
+ subject
+
+ expect(json_response.map { |package| package['id'] }).not_to include(hidden_package.id)
+ end
+ end
+
+ context 'no status param' do
+ it_behaves_like 'not including the hidden package'
+ end
+
+ context 'with hidden status param' do
+ let(:params) { super().merge(status: 'hidden') }
+
+ it 'returns the package' do
+ subject
+
+ expect(json_response.map { |package| package['id'] }).to include(hidden_package.id)
+ end
+ end
+ end
+
+ context 'bad status param' do
+ let(:params) { super().merge(status: 'invalid') }
+
+ it 'returns the package' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb b/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb
index f201c7b1780..1fb1b9f79b2 100644
--- a/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb
@@ -71,7 +71,7 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type|
it 'does not enqueue a GC run' do
expect { subject.execute }
- .not_to change(GitGarbageCollectWorker.jobs, :count)
+ .not_to change(Projects::GitGarbageCollectWorker.jobs, :count)
end
end
@@ -84,24 +84,29 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type|
stub_application_setting(housekeeping_enabled: false)
expect { subject.execute }
- .not_to change(GitGarbageCollectWorker.jobs, :count)
+ .not_to change(Projects::GitGarbageCollectWorker.jobs, :count)
end
it 'enqueues a GC run' do
expect { subject.execute }
- .to change(GitGarbageCollectWorker.jobs, :count).by(1)
+ .to change(Projects::GitGarbageCollectWorker.jobs, :count).by(1)
end
end
end
context 'when the filesystems are the same' do
- let(:destination) { project.repository_storage }
+ before do
+ expect(Gitlab::GitalyClient).to receive(:filesystem_id).twice.and_return(SecureRandom.uuid)
+ end
- it 'bails out and does nothing' do
+ it 'updates the database without trying to move the repostory', :aggregate_failures do
result = subject.execute
+ project.reload
- expect(result).to be_error
- expect(result.message).to match(/SameFilesystemError/)
+ expect(result).to be_success
+ expect(project).not_to be_repository_read_only
+ expect(project.repository_storage).to eq('test_second_storage')
+ expect(project.project_repository.shard_name).to eq('test_second_storage')
end
end
diff --git a/spec/support/shared_examples/services/repositories/housekeeping_shared_examples.rb b/spec/support/shared_examples/services/repositories/housekeeping_shared_examples.rb
index a174ae94b75..4c00faee56b 100644
--- a/spec/support/shared_examples/services/repositories/housekeeping_shared_examples.rb
+++ b/spec/support/shared_examples/services/repositories/housekeeping_shared_examples.rb
@@ -3,16 +3,16 @@
RSpec.shared_examples 'housekeeps repository' do
subject { described_class.new(resource) }
- context 'with a clean redis state', :clean_gitlab_redis_shared_state do
+ context 'with a clean redis state', :clean_gitlab_redis_shared_state, :aggregate_failures do
describe '#execute' do
it 'enqueues a sidekiq job' do
expect(subject).to receive(:try_obtain_lease).and_return(:the_uuid)
expect(subject).to receive(:lease_key).and_return(:the_lease_key)
expect(subject).to receive(:task).and_return(:incremental_repack)
- expect(GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :incremental_repack, :the_lease_key, :the_uuid).and_call_original
+ expect(resource.git_garbage_collect_worker_klass).to receive(:perform_async).with(resource.id, :incremental_repack, :the_lease_key, :the_uuid).and_call_original
Sidekiq::Testing.fake! do
- expect { subject.execute }.to change(GitGarbageCollectWorker.jobs, :size).by(1)
+ expect { subject.execute }.to change(resource.git_garbage_collect_worker_klass.jobs, :size).by(1)
end
end
@@ -38,7 +38,7 @@ RSpec.shared_examples 'housekeeps repository' do
end
it 'does not enqueue a job' do
- expect(GitGarbageCollectWorker).not_to receive(:perform_async)
+ expect(resource.git_garbage_collect_worker_klass).not_to receive(:perform_async)
expect { subject.execute }.to raise_error(Repositories::HousekeepingService::LeaseTaken)
end
@@ -63,16 +63,16 @@ RSpec.shared_examples 'housekeeps repository' do
allow(subject).to receive(:lease_key).and_return(:the_lease_key)
# At push 200
- expect(GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :gc, :the_lease_key, :the_uuid)
+ expect(resource.git_garbage_collect_worker_klass).to receive(:perform_async).with(resource.id, :gc, :the_lease_key, :the_uuid)
.once
# At push 50, 100, 150
- expect(GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :full_repack, :the_lease_key, :the_uuid)
+ expect(resource.git_garbage_collect_worker_klass).to receive(:perform_async).with(resource.id, :full_repack, :the_lease_key, :the_uuid)
.exactly(3).times
# At push 10, 20, ... (except those above)
- expect(GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :incremental_repack, :the_lease_key, :the_uuid)
+ expect(resource.git_garbage_collect_worker_klass).to receive(:perform_async).with(resource.id, :incremental_repack, :the_lease_key, :the_uuid)
.exactly(16).times
# At push 6, 12, 18, ... (except those above)
- expect(GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :pack_refs, :the_lease_key, :the_uuid)
+ expect(resource.git_garbage_collect_worker_klass).to receive(:perform_async).with(resource.id, :pack_refs, :the_lease_key, :the_uuid)
.exactly(27).times
201.times do
@@ -90,7 +90,7 @@ RSpec.shared_examples 'housekeeps repository' do
allow(housekeeping).to receive(:try_obtain_lease).and_return(:gc_uuid)
allow(housekeeping).to receive(:lease_key).and_return(:gc_lease_key)
- expect(GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :gc, :gc_lease_key, :gc_uuid).twice
+ expect(resource.git_garbage_collect_worker_klass).to receive(:perform_async).with(resource.id, :gc, :gc_lease_key, :gc_uuid).twice
2.times do
housekeeping.execute
diff --git a/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb b/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb
index d70ed707822..fac9f1d6253 100644
--- a/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb
@@ -3,8 +3,12 @@
RSpec.shared_examples 'timebox(milestone or iteration) resource events creator' do |timebox_event_class|
let_it_be(:user) { create(:user) }
+ before do
+ resource.system_note_timestamp = created_at_time
+ end
+
context 'when milestone/iteration is added' do
- let(:service) { described_class.new(resource, user, add_timebox_args) }
+ let(:service) { described_class.new(resource, user, **add_timebox_args) }
before do
set_timebox(timebox_event_class, timebox)
@@ -18,7 +22,7 @@ RSpec.shared_examples 'timebox(milestone or iteration) resource events creator'
end
context 'when milestone/iteration is removed' do
- let(:service) { described_class.new(resource, user, remove_timebox_args) }
+ let(:service) { described_class.new(resource, user, **remove_timebox_args) }
before do
set_timebox(timebox_event_class, nil)
diff --git a/spec/support/shared_examples/services/snippets_shared_examples.rb b/spec/support/shared_examples/services/snippets_shared_examples.rb
index 4a08c0d4365..10add3a7299 100644
--- a/spec/support/shared_examples/services/snippets_shared_examples.rb
+++ b/spec/support/shared_examples/services/snippets_shared_examples.rb
@@ -1,42 +1,56 @@
# frozen_string_literal: true
-RSpec.shared_examples 'snippets spam check is performed' do
- shared_examples 'marked as spam' do
- it 'marks a snippet as spam' do
- expect(snippet).to be_spam
- end
+RSpec.shared_examples 'checking spam' do
+ let(:request) { double(:request) }
+ let(:api) { true }
+ let(:captcha_response) { 'abc123' }
+ let(:spam_log_id) { 1 }
+ let(:disable_spam_action_service) { false }
- it 'invalidates the snippet' do
- expect(snippet).to be_invalid
- end
+ let(:extra_opts) do
+ {
+ request: request,
+ api: api,
+ captcha_response: captcha_response,
+ spam_log_id: spam_log_id,
+ disable_spam_action_service: disable_spam_action_service
+ }
+ end
- it 'creates a new spam_log' do
- expect { snippet }
- .to have_spam_log(title: snippet.title, noteable_type: snippet.class.name)
+ before do
+ allow_next_instance_of(UserAgentDetailService) do |instance|
+ allow(instance).to receive(:create)
end
+ end
- it 'assigns a spam_log to an issue' do
- expect(snippet.spam_log).to eq(SpamLog.last)
+ it 'executes SpamActionService' do
+ spam_params = Spam::SpamParams.new(
+ api: api,
+ captcha_response: captcha_response,
+ spam_log_id: spam_log_id
+ )
+ expect_next_instance_of(
+ Spam::SpamActionService,
+ {
+ spammable: kind_of(Snippet),
+ request: request,
+ user: an_instance_of(User),
+ action: action
+ }
+ ) do |instance|
+ expect(instance).to receive(:execute).with(spam_params: spam_params)
end
- end
- let(:extra_opts) do
- { visibility_level: Gitlab::VisibilityLevel::PUBLIC, request: double(:request, env: {}) }
+ subject
end
- before do
- expect_next_instance_of(Spam::AkismetService) do |akismet_service|
- expect(akismet_service).to receive_messages(spam?: true)
- end
- end
+ context 'when spam action service is disabled' do
+ let(:disable_spam_action_service) { true }
- [true, false, nil].each do |allow_possible_spam|
- context "when allow_possible_spam flag is #{allow_possible_spam.inspect}" do
- before do
- stub_feature_flags(allow_possible_spam: allow_possible_spam) unless allow_possible_spam.nil?
- end
+ it 'request parameter is not passed to the service' do
+ expect(Spam::SpamActionService).not_to receive(:new)
- it_behaves_like 'marked as spam'
+ subject
end
end
end
diff --git a/spec/support/shared_examples/workers/concerns/git_garbage_collect_methods_shared_examples.rb b/spec/support/shared_examples/workers/concerns/git_garbage_collect_methods_shared_examples.rb
new file mode 100644
index 00000000000..f2314793cb4
--- /dev/null
+++ b/spec/support/shared_examples/workers/concerns/git_garbage_collect_methods_shared_examples.rb
@@ -0,0 +1,320 @@
+# frozen_string_literal: true
+
+require 'fileutils'
+
+RSpec.shared_examples 'can collect git garbage' do |update_statistics: true|
+ include GitHelpers
+
+ let!(:lease_uuid) { SecureRandom.uuid }
+ let!(:lease_key) { "resource_housekeeping:#{resource.id}" }
+ let(:params) { [resource.id, task, lease_key, lease_uuid] }
+ let(:shell) { Gitlab::Shell.new }
+ let(:repository) { resource.repository }
+ let(:statistics_service_klass) { nil }
+
+ subject { described_class.new }
+
+ before do
+ allow(subject).to receive(:find_resource).and_return(resource)
+ end
+
+ shared_examples 'it calls Gitaly' do
+ specify do
+ repository_service = instance_double(Gitlab::GitalyClient::RepositoryService)
+
+ expect(subject).to receive(:get_gitaly_client).with(task, repository.raw_repository).and_return(repository_service)
+ expect(repository_service).to receive(gitaly_task)
+
+ subject.perform(*params)
+ end
+ end
+
+ shared_examples 'it updates the resource statistics' do
+ it 'updates the resource statistics' do
+ expect_next_instance_of(statistics_service_klass, anything, nil, statistics: statistics_keys) do |service|
+ expect(service).to receive(:execute)
+ end
+
+ subject.perform(*params)
+ end
+
+ it 'does nothing if the database is read-only' do
+ allow(Gitlab::Database).to receive(:read_only?) { true }
+
+ expect(statistics_service_klass).not_to receive(:new)
+
+ subject.perform(*params)
+ end
+ end
+
+ describe '#perform', :aggregate_failures do
+ let(:gitaly_task) { :garbage_collect }
+ let(:task) { :gc }
+
+ context 'with active lease_uuid' do
+ before do
+ allow(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
+ end
+
+ it_behaves_like 'it calls Gitaly'
+ it_behaves_like 'it updates the resource statistics' if update_statistics
+
+ it "flushes ref caches when the task if 'gc'" do
+ expect(subject).to receive(:renew_lease).with(lease_key, lease_uuid).and_call_original
+ expect(repository).to receive(:expire_branches_cache).and_call_original
+ expect(repository).to receive(:branch_names).and_call_original
+ expect(repository).to receive(:has_visible_content?).and_call_original
+ expect(repository.raw_repository).to receive(:has_visible_content?).and_call_original
+
+ subject.perform(*params)
+ end
+
+ it 'handles gRPC errors' do
+ allow_next_instance_of(Gitlab::GitalyClient::RepositoryService, repository.raw_repository) do |instance|
+ allow(instance).to receive(:garbage_collect).and_raise(GRPC::NotFound)
+ end
+
+ expect { subject.perform(*params) }.to raise_exception(Gitlab::Git::Repository::NoRepository)
+ end
+ end
+
+ context 'with different lease than the active one' do
+ before do
+ allow(subject).to receive(:get_lease_uuid).and_return(SecureRandom.uuid)
+ end
+
+ it 'returns silently' do
+ expect(repository).not_to receive(:expire_branches_cache).and_call_original
+ expect(repository).not_to receive(:branch_names).and_call_original
+ expect(repository).not_to receive(:has_visible_content?).and_call_original
+
+ subject.perform(*params)
+ end
+ end
+
+ context 'with no active lease' do
+ let(:params) { [resource.id] }
+
+ before do
+ allow(subject).to receive(:get_lease_uuid).and_return(false)
+ end
+
+ context 'when is able to get the lease' do
+ before do
+ allow(subject).to receive(:try_obtain_lease).and_return(SecureRandom.uuid)
+ end
+
+ it_behaves_like 'it calls Gitaly'
+ it_behaves_like 'it updates the resource statistics' if update_statistics
+
+ it "flushes ref caches when the task if 'gc'" do
+ expect(subject).to receive(:get_lease_uuid).with("git_gc:#{task}:#{expected_default_lease}").and_return(false)
+ expect(repository).to receive(:expire_branches_cache).and_call_original
+ expect(repository).to receive(:branch_names).and_call_original
+ expect(repository).to receive(:has_visible_content?).and_call_original
+ expect(repository.raw_repository).to receive(:has_visible_content?).and_call_original
+
+ subject.perform(*params)
+ end
+ end
+
+ context 'when no lease can be obtained' do
+ it 'returns silently' do
+ expect(subject).to receive(:try_obtain_lease).and_return(false)
+
+ expect(subject).not_to receive(:command)
+ expect(repository).not_to receive(:expire_branches_cache).and_call_original
+ expect(repository).not_to receive(:branch_names).and_call_original
+ expect(repository).not_to receive(:has_visible_content?).and_call_original
+
+ subject.perform(*params)
+ end
+ end
+ end
+
+ context 'repack_full' do
+ let(:task) { :full_repack }
+ let(:gitaly_task) { :repack_full }
+
+ before do
+ expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
+ end
+
+ it_behaves_like 'it calls Gitaly'
+ it_behaves_like 'it updates the resource statistics' if update_statistics
+ end
+
+ context 'pack_refs' do
+ let(:task) { :pack_refs }
+ let(:gitaly_task) { :pack_refs }
+
+ before do
+ expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
+ end
+
+ it 'calls Gitaly' do
+ repository_service = instance_double(Gitlab::GitalyClient::RefService)
+
+ expect(subject).to receive(:get_gitaly_client).with(task, repository.raw_repository).and_return(repository_service)
+ expect(repository_service).to receive(gitaly_task)
+
+ subject.perform(*params)
+ end
+
+ it 'does not update the resource statistics' do
+ expect(statistics_service_klass).not_to receive(:new)
+
+ subject.perform(*params)
+ end
+ end
+
+ context 'repack_incremental' do
+ let(:task) { :incremental_repack }
+ let(:gitaly_task) { :repack_incremental }
+
+ before do
+ expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
+ end
+
+ it_behaves_like 'it calls Gitaly'
+ it_behaves_like 'it updates the resource statistics' if update_statistics
+ end
+
+ shared_examples 'gc tasks' do
+ before do
+ allow(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
+ allow(subject).to receive(:bitmaps_enabled?).and_return(bitmaps_enabled)
+ end
+
+ it 'incremental repack adds a new packfile' do
+ create_objects(resource)
+ before_packs = packs(resource)
+
+ expect(before_packs.count).to be >= 1
+
+ subject.perform(resource.id, 'incremental_repack', lease_key, lease_uuid)
+ after_packs = packs(resource)
+
+ # Exactly one new pack should have been created
+ expect(after_packs.count).to eq(before_packs.count + 1)
+
+ # Previously existing packs are still around
+ expect(before_packs & after_packs).to eq(before_packs)
+ end
+
+ it 'full repack consolidates into 1 packfile' do
+ create_objects(resource)
+ subject.perform(resource.id, 'incremental_repack', lease_key, lease_uuid)
+ before_packs = packs(resource)
+
+ expect(before_packs.count).to be >= 2
+
+ subject.perform(resource.id, 'full_repack', lease_key, lease_uuid)
+ after_packs = packs(resource)
+
+ expect(after_packs.count).to eq(1)
+
+ # Previously existing packs should be gone now
+ expect(after_packs - before_packs).to eq(after_packs)
+
+ expect(File.exist?(bitmap_path(after_packs.first))).to eq(bitmaps_enabled)
+ end
+
+ it 'gc consolidates into 1 packfile and updates packed-refs' do
+ create_objects(resource)
+ before_packs = packs(resource)
+ before_packed_refs = packed_refs(resource)
+
+ expect(before_packs.count).to be >= 1
+
+ # It's quite difficult to use `expect_next_instance_of` in this place
+ # because the RepositoryService is instantiated several times to do
+ # some repository calls like `exists?`, `create_repository`, ... .
+ # Therefore, since we're instantiating the object several times,
+ # RSpec has troubles figuring out which instance is the next and which
+ # one we want to mock.
+ # Besides, at this point, we actually want to perform the call to Gitaly,
+ # otherwise we would just use `instance_double` like in other parts of the
+ # spec file.
+ expect_any_instance_of(Gitlab::GitalyClient::RepositoryService) # rubocop:disable RSpec/AnyInstanceOf
+ .to receive(:garbage_collect)
+ .with(bitmaps_enabled, prune: false)
+ .and_call_original
+
+ subject.perform(resource.id, 'gc', lease_key, lease_uuid)
+ after_packed_refs = packed_refs(resource)
+ after_packs = packs(resource)
+
+ expect(after_packs.count).to eq(1)
+
+ # Previously existing packs should be gone now
+ expect(after_packs - before_packs).to eq(after_packs)
+
+ # The packed-refs file should have been updated during 'git gc'
+ expect(before_packed_refs).not_to eq(after_packed_refs)
+
+ expect(File.exist?(bitmap_path(after_packs.first))).to eq(bitmaps_enabled)
+ end
+
+ it 'cleans up repository after finishing' do
+ expect(resource).to receive(:cleanup).and_call_original
+
+ subject.perform(resource.id, 'gc', lease_key, lease_uuid)
+ end
+
+ it 'prune calls garbage_collect with the option prune: true' do
+ repository_service = instance_double(Gitlab::GitalyClient::RepositoryService)
+
+ expect(subject).to receive(:get_gitaly_client).with(:prune, repository.raw_repository).and_return(repository_service)
+ expect(repository_service).to receive(:garbage_collect).with(bitmaps_enabled, prune: true)
+
+ subject.perform(resource.id, 'prune', lease_key, lease_uuid)
+ end
+
+ # Create a new commit on a random new branch
+ def create_objects(resource)
+ rugged = rugged_repo(resource.repository)
+ old_commit = rugged.branches.first.target
+ new_commit_sha = Rugged::Commit.create(
+ rugged,
+ message: "hello world #{SecureRandom.hex(6)}",
+ author: { email: 'foo@bar', name: 'baz' },
+ committer: { email: 'foo@bar', name: 'baz' },
+ tree: old_commit.tree,
+ parents: [old_commit]
+ )
+ rugged.references.create("refs/heads/#{SecureRandom.hex(6)}", new_commit_sha)
+ end
+
+ def packs(resource)
+ Dir["#{path_to_repo}/objects/pack/*.pack"]
+ end
+
+ def packed_refs(resource)
+ path = File.join(path_to_repo, 'packed-refs')
+ FileUtils.touch(path)
+ File.read(path)
+ end
+
+ def path_to_repo
+ @path_to_repo ||= File.join(TestEnv.repos_path, resource.repository.relative_path)
+ end
+
+ def bitmap_path(pack)
+ pack.sub(/\.pack\z/, '.bitmap')
+ end
+ end
+
+ context 'with bitmaps enabled' do
+ let(:bitmaps_enabled) { true }
+
+ include_examples 'gc tasks'
+ end
+
+ context 'with bitmaps disabled' do
+ let(:bitmaps_enabled) { false }
+
+ include_examples 'gc tasks'
+ end
+ end
+end
diff --git a/spec/tasks/gitlab/cleanup_rake_spec.rb b/spec/tasks/gitlab/cleanup_rake_spec.rb
index dc460611169..08d8651dcef 100644
--- a/spec/tasks/gitlab/cleanup_rake_spec.rb
+++ b/spec/tasks/gitlab/cleanup_rake_spec.rb
@@ -109,8 +109,7 @@ RSpec.describe 'gitlab:cleanup rake tasks' do
it 'passes dry_run correctly' do
expect(Gitlab::Cleanup::OrphanJobArtifactFiles)
.to receive(:new)
- .with(limit: anything,
- dry_run: false,
+ .with(dry_run: false,
niceness: anything,
logger: anything)
.and_call_original
@@ -145,7 +144,6 @@ RSpec.describe 'gitlab:cleanup rake tasks' do
expect(Gitlab::Cleanup::OrphanLfsFileReferences)
.to receive(:new)
.with(project,
- limit: anything,
dry_run: false,
logger: anything)
.and_call_original
@@ -153,24 +151,6 @@ RSpec.describe 'gitlab:cleanup rake tasks' do
rake_task
end
end
-
- context 'with LIMIT set to 100' do
- before do
- stub_env('LIMIT', '100')
- end
-
- it 'passes limit as integer' do
- expect(Gitlab::Cleanup::OrphanLfsFileReferences)
- .to receive(:new)
- .with(project,
- limit: 100,
- dry_run: true,
- logger: anything)
- .and_call_original
-
- rake_task
- end
- end
end
describe 'gitlab:cleanup:orphan_lfs_files' do
diff --git a/spec/tasks/gitlab/db_rake_spec.rb b/spec/tasks/gitlab/db_rake_spec.rb
index edfdb44022b..d1f4a12d8fc 100644
--- a/spec/tasks/gitlab/db_rake_spec.rb
+++ b/spec/tasks/gitlab/db_rake_spec.rb
@@ -297,6 +297,57 @@ RSpec.describe 'gitlab:db namespace rake task' do
end
end
+ describe '#migrate_with_instrumentation' do
+ subject { run_rake_task('gitlab:db:migration_testing', "[#{filename}]") }
+
+ let(:ctx) { double('ctx', migrations: all_migrations, schema_migration: double, get_all_versions: existing_versions) }
+ let(:instrumentation) { instance_double(Gitlab::Database::Migrations::Instrumentation, observations: observations) }
+ let(:existing_versions) { [1] }
+ let(:all_migrations) { [double('migration1', version: 1), pending_migration] }
+ let(:pending_migration) { double('migration2', version: 2) }
+ let(:filename) { 'results-file.json'}
+ let(:buffer) { StringIO.new }
+ let(:observations) { %w[some data] }
+
+ before do
+ allow(ActiveRecord::Base.connection).to receive(:migration_context).and_return(ctx)
+ allow(Gitlab::Database::Migrations::Instrumentation).to receive(:new).and_return(instrumentation)
+ allow(ActiveRecord::Migrator).to receive_message_chain('new.run').with(any_args).with(no_args)
+
+ allow(instrumentation).to receive(:observe).and_yield
+
+ allow(File).to receive(:open).with(filename, 'wb+').and_yield(buffer)
+ end
+
+ it 'fails when given no filename argument' do
+ expect { run_rake_task('gitlab:db:migration_testing') }.to raise_error(/specify result_file/)
+ end
+
+ it 'fails when the given file already exists' do
+ expect(File).to receive(:exist?).with(filename).and_return(true)
+
+ expect { subject }.to raise_error(/File exists/)
+ end
+
+ it 'instruments the pending migration' do
+ expect(instrumentation).to receive(:observe).with(2).and_yield
+
+ subject
+ end
+
+ it 'executes the pending migration' do
+ expect(ActiveRecord::Migrator).to receive_message_chain('new.run').with(:up, ctx.migrations, ctx.schema_migration, pending_migration.version).with(no_args)
+
+ subject
+ end
+
+ it 'writes observations out to JSON file' do
+ subject
+
+ expect(buffer.string).to eq(observations.to_json)
+ end
+ end
+
def run_rake_task(task_name, arguments = '')
Rake::Task[task_name].reenable
Rake.application.invoke_task("#{task_name}#{arguments}")
diff --git a/spec/tasks/gitlab/git_rake_spec.rb b/spec/tasks/gitlab/git_rake_spec.rb
index 4d89b126c9e..50ec2632d83 100644
--- a/spec/tasks/gitlab/git_rake_spec.rb
+++ b/spec/tasks/gitlab/git_rake_spec.rb
@@ -9,8 +9,6 @@ RSpec.describe 'gitlab:git rake tasks' do
before do
Rake.application.rake_require 'tasks/gitlab/git'
- allow_any_instance_of(String).to receive(:color) { |string, _color| string }
-
stub_warn_user_is_not_gitlab
end
diff --git a/spec/tasks/gitlab/pages_rake_spec.rb b/spec/tasks/gitlab/pages_rake_spec.rb
index 76808f52890..08194f4d1c9 100644
--- a/spec/tasks/gitlab/pages_rake_spec.rb
+++ b/spec/tasks/gitlab/pages_rake_spec.rb
@@ -2,66 +2,80 @@
require 'rake_helper'
-RSpec.describe 'gitlab:pages:migrate_legacy_storagerake task' do
+RSpec.describe 'gitlab:pages' do
before(:context) do
Rake.application.rake_require 'tasks/gitlab/pages'
end
- subject { run_rake_task('gitlab:pages:migrate_legacy_storage') }
+ describe 'migrate_legacy_storage task' do
+ subject { run_rake_task('gitlab:pages:migrate_legacy_storage') }
- let(:project) { create(:project) }
+ it 'calls migration service' do
+ expect_next_instance_of(::Pages::MigrateFromLegacyStorageService, anything,
+ migration_threads: 3,
+ batch_size: 10,
+ ignore_invalid_entries: false) do |service|
+ expect(service).to receive(:execute).and_call_original
+ end
- it 'does not try to migrate pages if pages are not deployed' do
- expect(::Pages::MigrateLegacyStorageToDeploymentService).not_to receive(:new)
+ subject
+ end
- subject
- end
+ it 'uses PAGES_MIGRATION_THREADS environment variable' do
+ stub_env('PAGES_MIGRATION_THREADS', '5')
- context 'when pages are marked as deployed' do
- before do
- project.mark_pages_as_deployed
+ expect_next_instance_of(::Pages::MigrateFromLegacyStorageService, anything,
+ migration_threads: 5,
+ batch_size: 10,
+ ignore_invalid_entries: false) do |service|
+ expect(service).to receive(:execute).and_call_original
+ end
+
+ subject
end
- context 'when pages directory does not exist' do
- it 'tries to migrate the project, but does not crash' do
- expect_next_instance_of(::Pages::MigrateLegacyStorageToDeploymentService, project) do |service|
- expect(service).to receive(:execute).and_call_original
- end
+ it 'uses PAGES_MIGRATION_BATCH_SIZE environment variable' do
+ stub_env('PAGES_MIGRATION_BATCH_SIZE', '100')
- subject
+ expect_next_instance_of(::Pages::MigrateFromLegacyStorageService, anything,
+ migration_threads: 3,
+ batch_size: 100,
+ ignore_invalid_entries: false) do |service|
+ expect(service).to receive(:execute).and_call_original
end
+
+ subject
end
- context 'when pages directory exists on disk' do
- before do
- FileUtils.mkdir_p File.join(project.pages_path, "public")
- File.open(File.join(project.pages_path, "public/index.html"), "w") do |f|
- f.write("Hello!")
- end
+ it 'uses PAGES_MIGRATION_IGNORE_INVALID_ENTRIES environment variable' do
+ stub_env('PAGES_MIGRATION_IGNORE_INVALID_ENTRIES', 'true')
+
+ expect_next_instance_of(::Pages::MigrateFromLegacyStorageService, anything,
+ migration_threads: 3,
+ batch_size: 10,
+ ignore_invalid_entries: true) do |service|
+ expect(service).to receive(:execute).and_call_original
end
- it 'migrates pages projects without deployments' do
- expect_next_instance_of(::Pages::MigrateLegacyStorageToDeploymentService, project) do |service|
- expect(service).to receive(:execute).and_call_original
- end
+ subject
+ end
+ end
+
+ describe 'clean_migrated_zip_storage task' do
+ it 'removes only migrated deployments' do
+ regular_deployment = create(:pages_deployment)
+ migrated_deployment = create(:pages_deployment, :migrated)
- expect do
- subject
- end.to change { project.pages_metadatum.reload.pages_deployment }.from(nil)
- end
+ regular_deployment.project.update_pages_deployment!(regular_deployment)
+ migrated_deployment.project.update_pages_deployment!(migrated_deployment)
- context 'when deployed already exists for the project' do
- before do
- deployment = create(:pages_deployment, project: project)
- project.set_first_pages_deployment!(deployment)
- end
+ expect(PagesDeployment.all).to contain_exactly(regular_deployment, migrated_deployment)
- it 'does not try to migrate project' do
- expect(::Pages::MigrateLegacyStorageToDeploymentService).not_to receive(:new)
+ run_rake_task('gitlab:pages:clean_migrated_zip_storage')
- subject
- end
- end
+ expect(PagesDeployment.all).to contain_exactly(regular_deployment)
+ expect(PagesDeployment.find_by_id(regular_deployment.id)).not_to be_nil
+ expect(PagesDeployment.find_by_id(migrated_deployment.id)).to be_nil
end
end
end
diff --git a/spec/tasks/gitlab/password_rake_spec.rb b/spec/tasks/gitlab/password_rake_spec.rb
new file mode 100644
index 00000000000..d5320f3b4af
--- /dev/null
+++ b/spec/tasks/gitlab/password_rake_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'rake_helper'
+
+RSpec.describe 'gitlab:password rake tasks' do
+ let_it_be(:user_1) { create(:user, username: 'foobar', password: 'initial_password') }
+
+ def stub_username(username)
+ allow(Gitlab::TaskHelpers).to receive(:prompt).with('Enter username: ').and_return(username)
+ end
+
+ def stub_password(password, confirmation = nil)
+ confirmation ||= password
+ allow(Gitlab::TaskHelpers).to receive(:prompt_for_password).and_return(password)
+ allow(Gitlab::TaskHelpers).to receive(:prompt_for_password).with('Confirm password: ').and_return(confirmation)
+ end
+
+ before do
+ Rake.application.rake_require 'tasks/gitlab/password'
+
+ stub_username('foobar')
+ stub_password('secretpassword')
+ end
+
+ describe ':reset' do
+ context 'when all inputs are correct' do
+ it 'updates the password properly' do
+ run_rake_task('gitlab:password:reset', user_1.username)
+ expect(user_1.reload.valid_password?('secretpassword')).to eq(true)
+ end
+ end
+
+ context 'when username is not provided' do
+ it 'asks for username' do
+ expect(Gitlab::TaskHelpers).to receive(:prompt).with('Enter username: ')
+
+ run_rake_task('gitlab:password:reset')
+ end
+
+ context 'when username is empty' do
+ it 'aborts with an error' do
+ stub_username('')
+ expect { run_rake_task('gitlab:password:reset') }.to raise_error(/Username can not be empty./)
+ end
+ end
+ end
+
+ context 'when username is passed as argument' do
+ it 'does not ask for username' do
+ expect(Gitlab::TaskHelpers).not_to receive(:prompt)
+
+ run_rake_task('gitlab:password:reset', 'foobar')
+ end
+ end
+
+ context 'when passwords do not match' do
+ before do
+ stub_password('randompassword', 'differentpassword')
+ end
+
+ it 'aborts with an error' do
+ expect { run_rake_task('gitlab:password:reset') }.to raise_error(%r{Unable to change password of the user with username foobar.\nPassword confirmation doesn't match Password})
+ end
+ end
+
+ context 'when user cannot be found' do
+ before do
+ stub_username('nonexistentuser')
+ end
+
+ it 'aborts with an error' do
+ expect { run_rake_task('gitlab:password:reset') }.to raise_error(/Unable to find user with username nonexistentuser./)
+ end
+ end
+ end
+end
diff --git a/spec/tasks/gitlab/terraform/migrate_rake_spec.rb b/spec/tasks/gitlab/terraform/migrate_rake_spec.rb
new file mode 100644
index 00000000000..4188521df8e
--- /dev/null
+++ b/spec/tasks/gitlab/terraform/migrate_rake_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'rake_helper'
+
+RSpec.describe 'gitlab:terraform_states' do
+ let_it_be(:version) { create(:terraform_state_version) }
+
+ let(:logger) { instance_double(Logger) }
+ let(:helper) { double }
+
+ before(:all) do
+ Rake.application.rake_require 'tasks/gitlab/terraform/migrate'
+ end
+
+ before do
+ allow(Logger).to receive(:new).with(STDOUT).and_return(logger)
+ end
+
+ describe 'gitlab:terraform_states:migrate' do
+ subject { run_rake_task('gitlab:terraform_states:migrate') }
+
+ it 'invokes the migration helper to move files to object storage' do
+ expect(Gitlab::Terraform::StateMigrationHelper).to receive(:migrate_to_remote_storage).and_yield(version)
+ expect(logger).to receive(:info).with('Starting transfer of Terraform states to object storage')
+ expect(logger).to receive(:info).with(/Transferred Terraform state version ID #{version.id}/)
+
+ subject
+ end
+
+ context 'an error is raised while migrating' do
+ let(:error_message) { 'Something went wrong' }
+
+ before do
+ allow(Gitlab::Terraform::StateMigrationHelper).to receive(:migrate_to_remote_storage).and_raise(StandardError, error_message)
+ end
+
+ it 'logs the error' do
+ expect(logger).to receive(:info).with('Starting transfer of Terraform states to object storage')
+ expect(logger).to receive(:error).with("Failed to migrate: #{error_message}")
+
+ subject
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/danger/base_linter_spec.rb b/spec/tooling/danger/base_linter_spec.rb
index 0136a0278ae..54d8f3dc1f7 100644
--- a/spec/lib/gitlab/danger/base_linter_spec.rb
+++ b/spec/tooling/danger/base_linter_spec.rb
@@ -1,12 +1,11 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
require 'rspec-parameterized'
require_relative 'danger_spec_helper'
-require 'gitlab/danger/base_linter'
+require_relative '../../../tooling/danger/base_linter'
-RSpec.describe Gitlab::Danger::BaseLinter do
+RSpec.describe Tooling::Danger::BaseLinter do
let(:commit_class) do
Struct.new(:message, :sha, :diff_parent)
end
diff --git a/spec/lib/gitlab/danger/changelog_spec.rb b/spec/tooling/danger/changelog_spec.rb
index 04c515f1205..c0eca67ce92 100644
--- a/spec/lib/gitlab/danger/changelog_spec.rb
+++ b/spec/tooling/danger/changelog_spec.rb
@@ -1,11 +1,10 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
require_relative 'danger_spec_helper'
-require 'gitlab/danger/changelog'
+require_relative '../../../tooling/danger/changelog'
-RSpec.describe Gitlab::Danger::Changelog do
+RSpec.describe Tooling::Danger::Changelog do
include DangerSpecHelper
let(:added_files) { nil }
@@ -53,8 +52,8 @@ RSpec.describe Gitlab::Danger::Changelog do
describe '#optional?' do
let(:category_with_changelog) { :backend }
let(:label_with_changelog) { 'frontend' }
- let(:category_without_changelog) { Gitlab::Danger::Changelog::NO_CHANGELOG_CATEGORIES.first }
- let(:label_without_changelog) { Gitlab::Danger::Changelog::NO_CHANGELOG_LABELS.first }
+ let(:category_without_changelog) { Tooling::Danger::Changelog::NO_CHANGELOG_CATEGORIES.first }
+ let(:label_without_changelog) { Tooling::Danger::Changelog::NO_CHANGELOG_LABELS.first }
subject { changelog.optional? }
diff --git a/spec/lib/gitlab/danger/commit_linter_spec.rb b/spec/tooling/danger/commit_linter_spec.rb
index d3d86037a53..694e524af21 100644
--- a/spec/lib/gitlab/danger/commit_linter_spec.rb
+++ b/spec/tooling/danger/commit_linter_spec.rb
@@ -1,12 +1,11 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
require 'rspec-parameterized'
require_relative 'danger_spec_helper'
-require 'gitlab/danger/commit_linter'
+require_relative '../../../tooling/danger/commit_linter'
-RSpec.describe Gitlab::Danger::CommitLinter do
+RSpec.describe Tooling::Danger::CommitLinter do
using RSpec::Parameterized::TableSyntax
let(:total_files_changed) { 2 }
diff --git a/spec/lib/gitlab/danger/danger_spec_helper.rb b/spec/tooling/danger/danger_spec_helper.rb
index b1e84b3c13d..b1e84b3c13d 100644
--- a/spec/lib/gitlab/danger/danger_spec_helper.rb
+++ b/spec/tooling/danger/danger_spec_helper.rb
diff --git a/spec/lib/gitlab/danger/emoji_checker_spec.rb b/spec/tooling/danger/emoji_checker_spec.rb
index 6092c751e1c..bbd957b3d00 100644
--- a/spec/lib/gitlab/danger/emoji_checker_spec.rb
+++ b/spec/tooling/danger/emoji_checker_spec.rb
@@ -1,11 +1,10 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
require 'rspec-parameterized'
-require 'gitlab/danger/emoji_checker'
+require_relative '../../../tooling/danger/emoji_checker'
-RSpec.describe Gitlab::Danger::EmojiChecker do
+RSpec.describe Tooling::Danger::EmojiChecker do
using RSpec::Parameterized::TableSyntax
describe '#includes_text_emoji?' do
diff --git a/spec/tooling/danger/feature_flag_spec.rb b/spec/tooling/danger/feature_flag_spec.rb
new file mode 100644
index 00000000000..db63116cc37
--- /dev/null
+++ b/spec/tooling/danger/feature_flag_spec.rb
@@ -0,0 +1,157 @@
+# frozen_string_literal: true
+
+require_relative 'danger_spec_helper'
+
+require_relative '../../../tooling/danger/feature_flag'
+
+RSpec.describe Tooling::Danger::FeatureFlag do
+ include DangerSpecHelper
+
+ let(:added_files) { nil }
+ let(:modified_files) { nil }
+ let(:deleted_files) { nil }
+ let(:fake_git) { double('fake-git', added_files: added_files, modified_files: modified_files, deleted_files: deleted_files) }
+
+ let(:mr_labels) { nil }
+ let(:mr_json) { nil }
+ let(:fake_gitlab) { double('fake-gitlab', mr_labels: mr_labels, mr_json: mr_json) }
+
+ let(:changes_by_category) { nil }
+ let(:sanitize_mr_title) { nil }
+ let(:ee?) { false }
+ let(:fake_helper) { double('fake-helper', changes_by_category: changes_by_category, sanitize_mr_title: sanitize_mr_title, ee?: ee?) }
+
+ let(:fake_danger) { new_fake_danger.include(described_class) }
+
+ subject(:feature_flag) { fake_danger.new(git: fake_git, gitlab: fake_gitlab, helper: fake_helper) }
+
+ describe '#feature_flag_files' do
+ let(:feature_flag_files) do
+ [
+ 'config/feature_flags/development/entry.yml',
+ 'ee/config/feature_flags/ops/entry.yml'
+ ]
+ end
+
+ let(:other_files) do
+ [
+ 'app/models/model.rb',
+ 'app/assets/javascripts/file.js'
+ ]
+ end
+
+ shared_examples 'an array of Found objects' do |change_type|
+ it 'returns an array of Found objects' do
+ expect(feature_flag.feature_flag_files(change_type: change_type)).to contain_exactly(an_instance_of(described_class::Found), an_instance_of(described_class::Found))
+ expect(feature_flag.feature_flag_files(change_type: change_type).map(&:path)).to eq(feature_flag_files)
+ end
+ end
+
+ shared_examples 'an empty array' do |change_type|
+ it 'returns an array of Found objects' do
+ expect(feature_flag.feature_flag_files(change_type: change_type)).to be_empty
+ end
+ end
+
+ describe 'retrieves added feature flag files' do
+ context 'with added added feature flag files' do
+ let(:added_files) { feature_flag_files }
+
+ include_examples 'an array of Found objects', :added
+ end
+
+ context 'without added added feature flag files' do
+ let(:added_files) { other_files }
+
+ include_examples 'an empty array', :added
+ end
+ end
+
+ describe 'retrieves modified feature flag files' do
+ context 'with modified modified feature flag files' do
+ let(:modified_files) { feature_flag_files }
+
+ include_examples 'an array of Found objects', :modified
+ end
+
+ context 'without modified modified feature flag files' do
+ let(:modified_files) { other_files }
+
+ include_examples 'an empty array', :modified
+ end
+ end
+
+ describe 'retrieves deleted feature flag files' do
+ context 'with deleted deleted feature flag files' do
+ let(:deleted_files) { feature_flag_files }
+
+ include_examples 'an array of Found objects', :deleted
+ end
+
+ context 'without deleted deleted feature flag files' do
+ let(:deleted_files) { other_files }
+
+ include_examples 'an empty array', :deleted
+ end
+ end
+ end
+
+ describe described_class::Found do
+ let(:feature_flag_path) { 'config/feature_flags/development/entry.yml' }
+ let(:group) { 'group::source code' }
+ let(:raw_yaml) do
+ YAML.dump('group' => group)
+ end
+
+ subject(:found) { described_class.new(feature_flag_path) }
+
+ before do
+ allow(File).to receive(:read).and_call_original
+ expect(File).to receive(:read).with(feature_flag_path).and_return(raw_yaml)
+ end
+
+ describe '#raw' do
+ it 'returns the raw YAML' do
+ expect(found.raw).to eq(raw_yaml)
+ end
+ end
+
+ describe '#group' do
+ it 'returns the group found in the YAML' do
+ expect(found.group).to eq(group)
+ end
+ end
+
+ describe '#group_match_mr_label?' do
+ subject(:result) { found.group_match_mr_label?(mr_group_label) }
+
+ context 'when MR labels match FF group' do
+ let(:mr_group_label) { 'group::source code' }
+
+ specify { expect(result).to eq(true) }
+ end
+
+ context 'when MR labels does not match FF group' do
+ let(:mr_group_label) { 'group::access' }
+
+ specify { expect(result).to eq(false) }
+ end
+
+ context 'when group is nil' do
+ let(:group) { nil }
+
+ context 'and MR has no group label' do
+ let(:mr_group_label) { nil }
+
+ specify { expect(result).to eq(true) }
+ end
+
+ context 'and MR has a group label' do
+ let(:mr_group_label) { 'group::source code' }
+
+ specify { expect(result).to eq(false) }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/tooling/danger/helper_spec.rb
index bd5c746dd54..c338d138352 100644
--- a/spec/lib/gitlab/danger/helper_spec.rb
+++ b/spec/tooling/danger/helper_spec.rb
@@ -4,9 +4,9 @@ require 'fast_spec_helper'
require 'rspec-parameterized'
require_relative 'danger_spec_helper'
-require 'gitlab/danger/helper'
+require_relative '../../../tooling/danger/helper'
-RSpec.describe Gitlab::Danger::Helper do
+RSpec.describe Tooling::Danger::Helper do
using RSpec::Parameterized::TableSyntax
include DangerSpecHelper
@@ -37,7 +37,7 @@ RSpec.describe Gitlab::Danger::Helper do
context 'when danger gitlab plugin is not available' do
it 'returns nil' do
invalid_danger = Class.new do
- include Gitlab::Danger::Helper
+ include Tooling::Danger::Helper
end.new
expect(invalid_danger.gitlab_helper).to be_nil
@@ -289,8 +289,8 @@ RSpec.describe Gitlab::Danger::Helper do
'.gitlab/ci/cng.gitlab-ci.yml' | [:engineering_productivity]
'.gitlab/ci/ee-specific-checks.gitlab-ci.yml' | [:engineering_productivity]
'scripts/foo' | [:engineering_productivity]
- 'lib/gitlab/danger/foo' | [:engineering_productivity]
- 'ee/lib/gitlab/danger/foo' | [:engineering_productivity]
+ 'tooling/danger/foo' | [:engineering_productivity]
+ 'ee/tooling/danger/foo' | [:engineering_productivity]
'lefthook.yml' | [:engineering_productivity]
'.editorconfig' | [:engineering_productivity]
'tooling/bin/find_foss_tests' | [:engineering_productivity]
@@ -402,13 +402,55 @@ RSpec.describe Gitlab::Danger::Helper do
end
end
- describe '#security_mr?' do
- it 'returns false when `gitlab_helper` is unavailable' do
+ describe '#mr_title' do
+ it 'returns "" when `gitlab_helper` is unavailable' do
expect(helper).to receive(:gitlab_helper).and_return(nil)
- expect(helper).not_to be_security_mr
+ expect(helper.mr_title).to eq('')
+ end
+
+ it 'returns the MR title when `gitlab_helper` is available' do
+ mr_title = 'My MR title'
+ expect(fake_gitlab).to receive(:mr_json)
+ .and_return('title' => mr_title)
+
+ expect(helper.mr_title).to eq(mr_title)
+ end
+ end
+
+ describe '#mr_web_url' do
+ it 'returns "" when `gitlab_helper` is unavailable' do
+ expect(helper).to receive(:gitlab_helper).and_return(nil)
+
+ expect(helper.mr_web_url).to eq('')
+ end
+
+ it 'returns the MR web_url when `gitlab_helper` is available' do
+ mr_web_url = 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1'
+ expect(fake_gitlab).to receive(:mr_json)
+ .and_return('web_url' => mr_web_url)
+
+ expect(helper.mr_web_url).to eq(mr_web_url)
+ end
+ end
+
+ describe '#mr_target_branch' do
+ it 'returns "" when `gitlab_helper` is unavailable' do
+ expect(helper).to receive(:gitlab_helper).and_return(nil)
+
+ expect(helper.mr_target_branch).to eq('')
end
+ it 'returns the MR web_url when `gitlab_helper` is available' do
+ mr_target_branch = 'main'
+ expect(fake_gitlab).to receive(:mr_json)
+ .and_return('target_branch' => mr_target_branch)
+
+ expect(helper.mr_target_branch).to eq(mr_target_branch)
+ end
+ end
+
+ describe '#security_mr?' do
it 'returns false when on a normal merge request' do
expect(fake_gitlab).to receive(:mr_json)
.and_return('web_url' => 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1')
@@ -425,12 +467,6 @@ RSpec.describe Gitlab::Danger::Helper do
end
describe '#draft_mr?' do
- it 'returns false when `gitlab_helper` is unavailable' do
- expect(helper).to receive(:gitlab_helper).and_return(nil)
-
- expect(helper).not_to be_draft_mr
- end
-
it 'returns true for a draft MR' do
expect(fake_gitlab).to receive(:mr_json)
.and_return('title' => 'Draft: My MR title')
@@ -447,12 +483,6 @@ RSpec.describe Gitlab::Danger::Helper do
end
describe '#cherry_pick_mr?' do
- it 'returns false when `gitlab_helper` is unavailable' do
- expect(helper).to receive(:gitlab_helper).and_return(nil)
-
- expect(helper).not_to be_cherry_pick_mr
- end
-
context 'when MR title does not mention a cherry-pick' do
it 'returns false' do
expect(fake_gitlab).to receive(:mr_json)
@@ -478,6 +508,46 @@ RSpec.describe Gitlab::Danger::Helper do
end
end
+ describe '#run_all_rspec_mr?' do
+ context 'when MR title does not mention RUN ALL RSPEC' do
+ it 'returns false' do
+ expect(fake_gitlab).to receive(:mr_json)
+ .and_return('title' => 'Add feature xyz')
+
+ expect(helper).not_to be_run_all_rspec_mr
+ end
+ end
+
+ context 'when MR title mentions RUN ALL RSPEC' do
+ it 'returns true' do
+ expect(fake_gitlab).to receive(:mr_json)
+ .and_return('title' => 'Add feature xyz RUN ALL RSPEC')
+
+ expect(helper).to be_run_all_rspec_mr
+ end
+ end
+ end
+
+ describe '#run_as_if_foss_mr?' do
+ context 'when MR title does not mention RUN AS-IF-FOSS' do
+ it 'returns false' do
+ expect(fake_gitlab).to receive(:mr_json)
+ .and_return('title' => 'Add feature xyz')
+
+ expect(helper).not_to be_run_as_if_foss_mr
+ end
+ end
+
+ context 'when MR title mentions RUN AS-IF-FOSS' do
+ it 'returns true' do
+ expect(fake_gitlab).to receive(:mr_json)
+ .and_return('title' => 'Add feature xyz RUN AS-IF-FOSS')
+
+ expect(helper).to be_run_as_if_foss_mr
+ end
+ end
+ end
+
describe '#stable_branch?' do
it 'returns false when `gitlab_helper` is unavailable' do
expect(helper).to receive(:gitlab_helper).and_return(nil)
@@ -599,4 +669,14 @@ RSpec.describe Gitlab::Danger::Helper do
end
end
end
+
+ describe '#group_label' do
+ it 'returns nil when no group label is present' do
+ expect(helper.group_label(%w[foo bar])).to be_nil
+ end
+
+ it 'returns the group label when a group label is present' do
+ expect(helper.group_label(['foo', 'group::source code', 'bar'])).to eq('group::source code')
+ end
+ end
end
diff --git a/spec/lib/gitlab/danger/merge_request_linter_spec.rb b/spec/tooling/danger/merge_request_linter_spec.rb
index 29facc9fdd6..3273b6b3d07 100644
--- a/spec/lib/gitlab/danger/merge_request_linter_spec.rb
+++ b/spec/tooling/danger/merge_request_linter_spec.rb
@@ -1,12 +1,11 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
require 'rspec-parameterized'
require_relative 'danger_spec_helper'
-require 'gitlab/danger/merge_request_linter'
+require_relative '../../../tooling/danger/merge_request_linter'
-RSpec.describe Gitlab::Danger::MergeRequestLinter do
+RSpec.describe Tooling::Danger::MergeRequestLinter do
using RSpec::Parameterized::TableSyntax
let(:mr_class) do
diff --git a/spec/lib/gitlab/danger/roulette_spec.rb b/spec/tooling/danger/roulette_spec.rb
index 59ac3b12b6b..1e500a1ed08 100644
--- a/spec/lib/gitlab/danger/roulette_spec.rb
+++ b/spec/tooling/danger/roulette_spec.rb
@@ -3,10 +3,10 @@
require 'webmock/rspec'
require 'timecop'
-require 'gitlab/danger/roulette'
+require_relative '../../../tooling/danger/roulette'
require 'active_support/testing/time_helpers'
-RSpec.describe Gitlab::Danger::Roulette do
+RSpec.describe Tooling::Danger::Roulette do
include ActiveSupport::Testing::TimeHelpers
around do |example|
@@ -16,7 +16,7 @@ RSpec.describe Gitlab::Danger::Roulette do
let(:backend_available) { true }
let(:backend_tz_offset_hours) { 2.0 }
let(:backend_maintainer) do
- Gitlab::Danger::Teammate.new(
+ Tooling::Danger::Teammate.new(
'username' => 'backend-maintainer',
'name' => 'Backend maintainer',
'role' => 'Backend engineer',
@@ -27,7 +27,7 @@ RSpec.describe Gitlab::Danger::Roulette do
end
let(:frontend_reviewer) do
- Gitlab::Danger::Teammate.new(
+ Tooling::Danger::Teammate.new(
'username' => 'frontend-reviewer',
'name' => 'Frontend reviewer',
'role' => 'Frontend engineer',
@@ -38,7 +38,7 @@ RSpec.describe Gitlab::Danger::Roulette do
end
let(:frontend_maintainer) do
- Gitlab::Danger::Teammate.new(
+ Tooling::Danger::Teammate.new(
'username' => 'frontend-maintainer',
'name' => 'Frontend maintainer',
'role' => 'Frontend engineer',
@@ -49,18 +49,18 @@ RSpec.describe Gitlab::Danger::Roulette do
end
let(:software_engineer_in_test) do
- Gitlab::Danger::Teammate.new(
+ Tooling::Danger::Teammate.new(
'username' => 'software-engineer-in-test',
'name' => 'Software Engineer in Test',
'role' => 'Software Engineer in Test, Create:Source Code',
- 'projects' => { 'gitlab' => 'reviewer qa', 'gitlab-qa' => 'maintainer' },
+ 'projects' => { 'gitlab' => 'maintainer qa', 'gitlab-qa' => 'maintainer' },
'available' => true,
'tz_offset_hours' => 2.0
)
end
let(:engineering_productivity_reviewer) do
- Gitlab::Danger::Teammate.new(
+ Tooling::Danger::Teammate.new(
'username' => 'eng-prod-reviewer',
'name' => 'EP engineer',
'role' => 'Engineering Productivity',
@@ -71,7 +71,7 @@ RSpec.describe Gitlab::Danger::Roulette do
end
let(:ci_template_reviewer) do
- Gitlab::Danger::Teammate.new(
+ Tooling::Danger::Teammate.new(
'username' => 'ci-template-maintainer',
'name' => 'CI Template engineer',
'role' => '~"ci::templates"',
@@ -121,7 +121,7 @@ RSpec.describe Gitlab::Danger::Roulette do
let!(:project) { 'gitlab' }
let!(:mr_source_branch) { 'a-branch' }
let!(:mr_labels) { ['backend', 'devops::create'] }
- let!(:author) { Gitlab::Danger::Teammate.new('username' => 'johndoe') }
+ let!(:author) { Tooling::Danger::Teammate.new('username' => 'johndoe') }
let(:timezone_experiment) { false }
let(:spins) do
# Stub the request at the latest time so that we can modify the raw data, e.g. available fields.
@@ -176,8 +176,24 @@ RSpec.describe Gitlab::Danger::Roulette do
context 'when change contains QA category' do
let(:categories) { [:qa] }
- it 'assigns QA reviewer' do
- expect(spins).to eq([described_class::Spin.new(:qa, software_engineer_in_test, nil, false, false)])
+ it 'assigns QA maintainer' do
+ expect(spins).to eq([described_class::Spin.new(:qa, nil, software_engineer_in_test, false, false)])
+ end
+ end
+
+ context 'when change contains QA category and another category' do
+ let(:categories) { [:backend, :qa] }
+
+ it 'assigns QA maintainer' do
+ expect(spins).to eq([described_class::Spin.new(:backend, engineering_productivity_reviewer, backend_maintainer, false, false), described_class::Spin.new(:qa, nil, software_engineer_in_test, :maintainer, false)])
+ end
+
+ context 'and author is an SET' do
+ let!(:author) { Tooling::Danger::Teammate.new('username' => software_engineer_in_test.username) }
+
+ it 'assigns QA reviewer' do
+ expect(spins).to eq([described_class::Spin.new(:backend, engineering_productivity_reviewer, backend_maintainer, false, false), described_class::Spin.new(:qa, nil, nil, false, false)])
+ end
end
end
@@ -330,7 +346,7 @@ RSpec.describe Gitlab::Danger::Roulette do
describe '#spin_for_person' do
let(:person_tz_offset_hours) { 0.0 }
let(:person1) do
- Gitlab::Danger::Teammate.new(
+ Tooling::Danger::Teammate.new(
'username' => 'user1',
'available' => true,
'tz_offset_hours' => person_tz_offset_hours
@@ -338,21 +354,21 @@ RSpec.describe Gitlab::Danger::Roulette do
end
let(:person2) do
- Gitlab::Danger::Teammate.new(
+ Tooling::Danger::Teammate.new(
'username' => 'user2',
'available' => true,
'tz_offset_hours' => person_tz_offset_hours)
end
let(:author) do
- Gitlab::Danger::Teammate.new(
+ Tooling::Danger::Teammate.new(
'username' => 'johndoe',
'available' => true,
'tz_offset_hours' => 0.0)
end
let(:unavailable) do
- Gitlab::Danger::Teammate.new(
+ Tooling::Danger::Teammate.new(
'username' => 'janedoe',
'available' => false,
'tz_offset_hours' => 0.0)
diff --git a/spec/lib/gitlab/danger/sidekiq_queues_spec.rb b/spec/tooling/danger/sidekiq_queues_spec.rb
index 7dd1a2e6924..c5fc8592621 100644
--- a/spec/lib/gitlab/danger/sidekiq_queues_spec.rb
+++ b/spec/tooling/danger/sidekiq_queues_spec.rb
@@ -1,12 +1,11 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
require 'rspec-parameterized'
require_relative 'danger_spec_helper'
-require 'gitlab/danger/sidekiq_queues'
+require_relative '../../../tooling/danger/sidekiq_queues'
-RSpec.describe Gitlab::Danger::SidekiqQueues do
+RSpec.describe Tooling::Danger::SidekiqQueues do
using RSpec::Parameterized::TableSyntax
include DangerSpecHelper
diff --git a/spec/lib/gitlab/danger/teammate_spec.rb b/spec/tooling/danger/teammate_spec.rb
index 9c066ba4c1b..f3afdc6e912 100644
--- a/spec/lib/gitlab/danger/teammate_spec.rb
+++ b/spec/tooling/danger/teammate_spec.rb
@@ -1,12 +1,10 @@
# frozen_string_literal: true
-require 'timecop'
-require 'rspec-parameterized'
-
-require 'gitlab/danger/teammate'
+require_relative '../../../tooling/danger/teammate'
require 'active_support/testing/time_helpers'
+require 'rspec-parameterized'
-RSpec.describe Gitlab::Danger::Teammate do
+RSpec.describe Tooling::Danger::Teammate do
using RSpec::Parameterized::TableSyntax
subject { described_class.new(options) }
@@ -51,6 +49,13 @@ RSpec.describe Gitlab::Danger::Teammate do
context 'when having multiple capabilities' do
let(:capabilities) { ['reviewer backend', 'maintainer frontend', 'trainee_maintainer qa'] }
+ it '#any_capability? returns true if the person has any capability for the category in the given project' do
+ expect(subject.any_capability?(project, :backend)).to be_truthy
+ expect(subject.any_capability?(project, :frontend)).to be_truthy
+ expect(subject.any_capability?(project, :qa)).to be_truthy
+ expect(subject.any_capability?(project, :engineering_productivity)).to be_falsey
+ end
+
it '#reviewer? supports multiple roles per project' do
expect(subject.reviewer?(project, :backend, labels)).to be_truthy
end
diff --git a/spec/lib/gitlab/danger/title_linting_spec.rb b/spec/tooling/danger/title_linting_spec.rb
index b48d2c5e53d..7bc1684cd87 100644
--- a/spec/lib/gitlab/danger/title_linting_spec.rb
+++ b/spec/tooling/danger/title_linting_spec.rb
@@ -1,11 +1,10 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
require 'rspec-parameterized'
-require 'gitlab/danger/title_linting'
+require_relative '../../../tooling/danger/title_linting'
-RSpec.describe Gitlab::Danger::TitleLinting do
+RSpec.describe Tooling::Danger::TitleLinting do
using RSpec::Parameterized::TableSyntax
describe '#sanitize_mr_title' do
@@ -53,4 +52,40 @@ RSpec.describe Gitlab::Danger::TitleLinting do
expect(described_class.has_draft_flag?('My MR title')).to be false
end
end
+
+ describe '#has_cherry_pick_flag?' do
+ [
+ 'Cherry Pick !1234',
+ 'cherry-pick !1234',
+ 'CherryPick !1234'
+ ].each do |mr_title|
+ it 'returns true for cherry-pick title' do
+ expect(described_class.has_cherry_pick_flag?(mr_title)).to be true
+ end
+ end
+
+ it 'returns false for non cherry-pick title' do
+ expect(described_class.has_cherry_pick_flag?('My MR title')).to be false
+ end
+ end
+
+ describe '#has_run_all_rspec_flag?' do
+ it 'returns true for a title that includes RUN ALL RSPEC' do
+ expect(described_class.has_run_all_rspec_flag?('My MR title RUN ALL RSPEC')).to be true
+ end
+
+ it 'returns true for a title that does not include RUN ALL RSPEC' do
+ expect(described_class.has_run_all_rspec_flag?('My MR title')).to be false
+ end
+ end
+
+ describe '#has_run_as_if_foss_flag?' do
+ it 'returns true for a title that includes RUN AS-IF-FOSS' do
+ expect(described_class.has_run_as_if_foss_flag?('My MR title RUN AS-IF-FOSS')).to be true
+ end
+
+ it 'returns true for a title that does not include RUN AS-IF-FOSS' do
+ expect(described_class.has_run_as_if_foss_flag?('My MR title')).to be false
+ end
+ end
end
diff --git a/spec/lib/gitlab/danger/weightage/maintainers_spec.rb b/spec/tooling/danger/weightage/maintainers_spec.rb
index 066bb487fa2..b99ffe706a4 100644
--- a/spec/lib/gitlab/danger/weightage/maintainers_spec.rb
+++ b/spec/tooling/danger/weightage/maintainers_spec.rb
@@ -1,9 +1,9 @@
# frozen_string_literal: true
-require 'gitlab/danger/weightage/maintainers'
+require_relative '../../../../tooling/danger/weightage/maintainers'
-RSpec.describe Gitlab::Danger::Weightage::Maintainers do
- let(:multiplier) { Gitlab::Danger::Weightage::CAPACITY_MULTIPLIER }
+RSpec.describe Tooling::Danger::Weightage::Maintainers do
+ let(:multiplier) { Tooling::Danger::Weightage::CAPACITY_MULTIPLIER }
let(:regular_maintainer) { double('Teammate', reduced_capacity: false) }
let(:reduced_capacity_maintainer) { double('Teammate', reduced_capacity: true) }
let(:maintainers) do
@@ -13,8 +13,8 @@ RSpec.describe Gitlab::Danger::Weightage::Maintainers do
]
end
- let(:maintainer_count) { Gitlab::Danger::Weightage::BASE_REVIEWER_WEIGHT * multiplier }
- let(:reduced_capacity_maintainer_count) { Gitlab::Danger::Weightage::BASE_REVIEWER_WEIGHT }
+ let(:maintainer_count) { Tooling::Danger::Weightage::BASE_REVIEWER_WEIGHT * multiplier }
+ let(:reduced_capacity_maintainer_count) { Tooling::Danger::Weightage::BASE_REVIEWER_WEIGHT }
subject(:weighted_maintainers) { described_class.new(maintainers).execute }
diff --git a/spec/lib/gitlab/danger/weightage/reviewers_spec.rb b/spec/tooling/danger/weightage/reviewers_spec.rb
index cca81f4d9b5..5693ce7a10c 100644
--- a/spec/lib/gitlab/danger/weightage/reviewers_spec.rb
+++ b/spec/tooling/danger/weightage/reviewers_spec.rb
@@ -1,9 +1,9 @@
# frozen_string_literal: true
-require 'gitlab/danger/weightage/reviewers'
+require_relative '../../../../tooling/danger/weightage/reviewers'
-RSpec.describe Gitlab::Danger::Weightage::Reviewers do
- let(:multiplier) { Gitlab::Danger::Weightage::CAPACITY_MULTIPLIER }
+RSpec.describe Tooling::Danger::Weightage::Reviewers do
+ let(:multiplier) { Tooling::Danger::Weightage::CAPACITY_MULTIPLIER }
let(:regular_reviewer) { double('Teammate', hungry: false, reduced_capacity: false) }
let(:hungry_reviewer) { double('Teammate', hungry: true, reduced_capacity: false) }
let(:reduced_capacity_reviewer) { double('Teammate', hungry: false, reduced_capacity: true) }
@@ -26,11 +26,11 @@ RSpec.describe Gitlab::Danger::Weightage::Reviewers do
]
end
- let(:hungry_reviewer_count) { Gitlab::Danger::Weightage::BASE_REVIEWER_WEIGHT * multiplier + described_class::DEFAULT_REVIEWER_WEIGHT }
+ let(:hungry_reviewer_count) { Tooling::Danger::Weightage::BASE_REVIEWER_WEIGHT * multiplier + described_class::DEFAULT_REVIEWER_WEIGHT }
let(:hungry_traintainer_count) { described_class::TRAINTAINER_WEIGHT * multiplier + described_class::DEFAULT_REVIEWER_WEIGHT }
- let(:reviewer_count) { Gitlab::Danger::Weightage::BASE_REVIEWER_WEIGHT * multiplier }
- let(:traintainer_count) { Gitlab::Danger::Weightage::BASE_REVIEWER_WEIGHT * described_class::TRAINTAINER_WEIGHT * multiplier }
- let(:reduced_capacity_reviewer_count) { Gitlab::Danger::Weightage::BASE_REVIEWER_WEIGHT }
+ let(:reviewer_count) { Tooling::Danger::Weightage::BASE_REVIEWER_WEIGHT * multiplier }
+ let(:traintainer_count) { Tooling::Danger::Weightage::BASE_REVIEWER_WEIGHT * described_class::TRAINTAINER_WEIGHT * multiplier }
+ let(:reduced_capacity_reviewer_count) { Tooling::Danger::Weightage::BASE_REVIEWER_WEIGHT }
let(:reduced_capacity_traintainer_count) { described_class::TRAINTAINER_WEIGHT }
subject(:weighted_reviewers) { described_class.new(reviewers, traintainers).execute }
diff --git a/spec/lib/gitlab_danger_spec.rb b/spec/tooling/gitlab_danger_spec.rb
index ed668c52a0e..20ac40d1d2a 100644
--- a/spec/lib/gitlab_danger_spec.rb
+++ b/spec/tooling/gitlab_danger_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require_relative '../../tooling/gitlab_danger'
RSpec.describe GitlabDanger do
let(:gitlab_danger_helper) { nil }
diff --git a/spec/tooling/lib/tooling/kubernetes_client_spec.rb b/spec/tooling/lib/tooling/kubernetes_client_spec.rb
index 2511295206c..4a84ec09b5c 100644
--- a/spec/tooling/lib/tooling/kubernetes_client_spec.rb
+++ b/spec/tooling/lib/tooling/kubernetes_client_spec.rb
@@ -29,7 +29,7 @@ RSpec.describe Tooling::KubernetesClient do
specify do
expect(Gitlab::Popen).to receive(:popen_with_detail)
.with(["kubectl delete #{described_class::RESOURCE_LIST} " +
- %(--namespace "#{namespace}" --now --ignore-not-found --include-uninitialized --wait=#{wait} #{release_names_in_command})])
+ %(--namespace "#{namespace}" --now --ignore-not-found --wait=#{wait} #{release_names_in_command})])
.and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
expect(Gitlab::Popen).to receive(:popen_with_detail)
@@ -44,7 +44,7 @@ RSpec.describe Tooling::KubernetesClient do
it 'raises an error if the Kubernetes command fails' do
expect(Gitlab::Popen).to receive(:popen_with_detail)
.with(["kubectl delete #{described_class::RESOURCE_LIST} " +
- %(--namespace "#{namespace}" --now --ignore-not-found --include-uninitialized --wait=true -l release="#{release_name}")])
+ %(--namespace "#{namespace}" --now --ignore-not-found --wait=true -l release="#{release_name}")])
.and_return(Gitlab::Popen::Result.new([], '', '', double(success?: false)))
expect { subject.cleanup_by_release(release_name: release_name) }.to raise_error(described_class::CommandFailedError)
@@ -81,7 +81,7 @@ RSpec.describe Tooling::KubernetesClient do
specify do
expect(Gitlab::Popen).to receive(:popen_with_detail)
.with(["kubectl delete #{resource_type} ".squeeze(' ') +
- %(--namespace "#{namespace}" --now --ignore-not-found --include-uninitialized --wait=#{wait} #{release_names_in_command})])
+ %(--namespace "#{namespace}" --now --ignore-not-found --wait=#{wait} #{release_names_in_command})])
.and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
# We're not verifying the output here, just silencing it
@@ -92,7 +92,7 @@ RSpec.describe Tooling::KubernetesClient do
it 'raises an error if the Kubernetes command fails' do
expect(Gitlab::Popen).to receive(:popen_with_detail)
.with(["kubectl delete #{resource_type} " +
- %(--namespace "#{namespace}" --now --ignore-not-found --include-uninitialized --wait=true #{pod_for_release})])
+ %(--namespace "#{namespace}" --now --ignore-not-found --wait=true #{pod_for_release})])
.and_return(Gitlab::Popen::Result.new([], '', '', double(success?: false)))
expect { subject.cleanup_by_created_at(resource_type: resource_type, created_before: two_days_ago) }.to raise_error(described_class::CommandFailedError)
diff --git a/spec/uploaders/packages/composer/cache_uploader_spec.rb b/spec/uploaders/packages/composer/cache_uploader_spec.rb
new file mode 100644
index 00000000000..a4ba4cc2a1e
--- /dev/null
+++ b/spec/uploaders/packages/composer/cache_uploader_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::Composer::CacheUploader do
+ let(:cache_file) { create(:composer_cache_file) } # rubocop:disable Rails/SaveBang
+ let(:uploader) { described_class.new(cache_file, :file) }
+ let(:path) { Gitlab.config.packages.storage_path }
+
+ subject { uploader }
+
+ it_behaves_like "builds correct paths",
+ store_dir: %r[^\h{2}/\h{2}/\h{64}/packages/composer_cache/\d+$],
+ cache_dir: %r[/packages/tmp/cache],
+ work_dir: %r[/packages/tmp/work]
+
+ context 'object store is remote' do
+ before do
+ stub_composer_cache_object_storage
+ end
+
+ include_context 'with storage', described_class::Store::REMOTE
+
+ it_behaves_like "builds correct paths",
+ store_dir: %r[^\h{2}/\h{2}/\h{64}/packages/composer_cache/\d+$]
+ end
+
+ describe 'remote file' do
+ let(:cache_file) { create(:composer_cache_file, :object_storage) }
+
+ context 'with object storage enabled' do
+ before do
+ stub_composer_cache_object_storage
+ end
+
+ it 'can store file remotely' do
+ allow(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async)
+
+ cache_file
+
+ expect(cache_file.file_store).to eq(described_class::Store::REMOTE)
+ expect(cache_file.file.path).not_to be_blank
+ end
+ end
+ end
+end
diff --git a/spec/uploaders/packages/debian/component_file_uploader_spec.rb b/spec/uploaders/packages/debian/component_file_uploader_spec.rb
new file mode 100644
index 00000000000..de60ec94acf
--- /dev/null
+++ b/spec/uploaders/packages/debian/component_file_uploader_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::Debian::ComponentFileUploader do
+ [:project, :group].each do |container_type|
+ context "Packages::Debian::#{container_type.capitalize}ComponentFile" do
+ let(:factory) { "debian_#{container_type}_component_file" }
+ let(:component_file) { create(factory) } # rubocop:disable Rails/SaveBang
+ let(:uploader) { described_class.new(component_file, :file) }
+ let(:path) { Gitlab.config.packages.storage_path }
+
+ subject { uploader }
+
+ it_behaves_like "builds correct paths",
+ store_dir: %r[^\h{2}/\h{2}/\h{64}/debian_#{container_type}_component_file/\d+$],
+ cache_dir: %r[/packages/tmp/cache$],
+ work_dir: %r[/packages/tmp/work$]
+
+ context 'object store is remote' do
+ before do
+ stub_package_file_object_storage
+ end
+
+ include_context 'with storage', described_class::Store::REMOTE
+
+ it_behaves_like "builds correct paths",
+ store_dir: %r[^\h{2}/\h{2}/\h{64}/debian_#{container_type}_component_file/\d+$],
+ cache_dir: %r[/packages/tmp/cache$],
+ work_dir: %r[/packages/tmp/work$]
+ end
+
+ describe 'remote file' do
+ let(:component_file) { create(factory, :object_storage) }
+
+ context 'with object storage enabled' do
+ before do
+ stub_package_file_object_storage
+ end
+
+ it 'can store file remotely' do
+ allow(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async)
+
+ component_file
+
+ expect(component_file.file_store).to eq(described_class::Store::REMOTE)
+ expect(component_file.file.path).not_to be_blank
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/validators/variable_duplicates_validator_spec.rb b/spec/validators/nested_attributes_duplicates_validator_spec.rb
index acc47ff225f..f59e422e5d4 100644
--- a/spec/validators/variable_duplicates_validator_spec.rb
+++ b/spec/validators/nested_attributes_duplicates_validator_spec.rb
@@ -2,13 +2,16 @@
require 'spec_helper'
-RSpec.describe VariableDuplicatesValidator do
- let(:validator) { described_class.new(attributes: [:variables], **options) }
+RSpec.describe NestedAttributesDuplicatesValidator do
+ let(:validator) { described_class.new(attributes: [attribute], **options) }
describe '#validate_each' do
let(:project) { build(:project) }
+ let(:record) { project }
+ let(:attribute) { :variables }
+ let(:value) { project.variables }
- subject { validator.validate_each(project, :variables, project.variables) }
+ subject { validator.validate_each(record, attribute, value) }
context 'with no scope' do
let(:options) { {} }
@@ -65,5 +68,46 @@ RSpec.describe VariableDuplicatesValidator do
end
end
end
+
+ context 'with a child attribute' do
+ let(:release) { build(:release) }
+ let(:first_link) { build(:release_link, name: 'test1', url: 'https://www.google1.com', release: release) }
+ let(:second_link) { build(:release_link, name: 'test2', url: 'https://www.google2.com', release: release) }
+ let(:record) { release }
+ let(:attribute) { :links }
+ let(:value) { release.links }
+ let(:options) { { scope: :release, child_attributes: %i[name url] } }
+
+ before do
+ release.links << first_link
+ release.links << second_link
+ end
+
+ it 'does not have any errors' do
+ subject
+
+ expect(release.errors.empty?).to be true
+ end
+
+ context 'when name is duplicated' do
+ let(:second_link) { build(:release_link, name: 'test1', release: release) }
+
+ it 'has a duplicate error' do
+ subject
+
+ expect(release.errors).to have_key(attribute)
+ end
+ end
+
+ context 'when url is duplicated' do
+ let(:second_link) { build(:release_link, url: 'https://www.google1.com', release: release) }
+
+ it 'has a duplicate error' do
+ subject
+
+ expect(release.errors).to have_key(attribute)
+ end
+ end
+ end
end
end
diff --git a/spec/views/groups/show.html.haml_spec.rb b/spec/views/groups/show.html.haml_spec.rb
new file mode 100644
index 00000000000..a53aab43c18
--- /dev/null
+++ b/spec/views/groups/show.html.haml_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'groups/show.html.haml' do
+ let_it_be(:user) { build(:user) }
+ let_it_be(:group) { create(:group) }
+
+ before do
+ assign(:group, group)
+ end
+
+ context 'when rendering with the layout' do
+ subject(:render_page) { render template: 'groups/show.html.haml', layout: 'layouts/group' }
+
+ describe 'invite team members' do
+ before do
+ allow(view).to receive(:session).and_return({})
+ allow(view).to receive(:current_user_mode).and_return(Gitlab::Auth::CurrentUserMode.new(user))
+ allow(view).to receive(:current_user).and_return(user)
+ allow(view).to receive(:experiment_enabled?).and_return(false)
+ allow(view).to receive(:group_path).and_return('')
+ allow(view).to receive(:group_shared_path).and_return('')
+ allow(view).to receive(:group_archived_path).and_return('')
+ end
+
+ context 'when invite team members is not available in sidebar' do
+ before do
+ allow(view).to receive(:can_invite_members_for_group?).and_return(false)
+ end
+
+ it 'does not display the js-invite-members-trigger' do
+ render_page
+
+ expect(rendered).not_to have_selector('.js-invite-members-trigger')
+ end
+ end
+
+ context 'when invite team members is available' do
+ before do
+ allow(view).to receive(:can_invite_members_for_group?).and_return(true)
+ end
+
+ it 'includes the div for js-invite-members-trigger' do
+ render_page
+
+ expect(rendered).to have_selector('.js-invite-members-trigger')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/views/layouts/_head.html.haml_spec.rb b/spec/views/layouts/_head.html.haml_spec.rb
index 15fdfaaaa65..6752bdc8337 100644
--- a/spec/views/layouts/_head.html.haml_spec.rb
+++ b/spec/views/layouts/_head.html.haml_spec.rb
@@ -92,7 +92,8 @@ RSpec.describe 'layouts/_head' do
before do
stub_config(extra: {
matomo_url: matomo_host,
- matomo_site_id: 12345
+ matomo_site_id: 12345,
+ matomo_disable_cookies: false
})
end
@@ -101,6 +102,19 @@ RSpec.describe 'layouts/_head' do
expect(rendered).to match(/<script.*>.*var u="\/\/#{matomo_host}\/".*<\/script>/m)
expect(rendered).to match(%r(<noscript>.*<img src="//#{matomo_host}/matomo.php.*</noscript>))
+ expect(rendered).not_to include('_paq.push(["disableCookies"])')
+ end
+
+ context 'when matomo_disable_cookies is true' do
+ before do
+ stub_config(extra: { matomo_url: matomo_host, matomo_site_id: 12345, matomo_disable_cookies: true })
+ end
+
+ it 'disables cookies' do
+ render
+
+ expect(rendered).to include('_paq.push(["disableCookies"])')
+ end
end
end
diff --git a/spec/views/layouts/header/_new_dropdown.haml_spec.rb b/spec/views/layouts/header/_new_dropdown.haml_spec.rb
index 01892e72c97..80342cbdb41 100644
--- a/spec/views/layouts/header/_new_dropdown.haml_spec.rb
+++ b/spec/views/layouts/header/_new_dropdown.haml_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe 'layouts/header/_new_dropdown' do
before do
allow(Gitlab::Experimentation).to receive(:active?).and_return(true)
allow(view).to receive(:experiment_tracking_category_and_group)
- allow(view).to receive(:tracking_label).with(user)
+ allow(view).to receive(:tracking_label)
end
context 'with ability to invite members' do
@@ -20,8 +20,8 @@ RSpec.describe 'layouts/header/_new_dropdown' do
subject
expect(view).to have_received(:experiment_tracking_category_and_group)
- .with(:invite_members_new_dropdown, subject: user)
- expect(view).to have_received(:tracking_label).with(user)
+ .with(:invite_members_new_dropdown)
+ expect(view).to have_received(:tracking_label)
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 c5b56b15431..e34d8b91b38 100644
--- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
+++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
@@ -48,7 +48,7 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
end
describe 'Packages' do
- let(:user) { create(:user) }
+ let_it_be(:user) { create(:user) }
let_it_be(:package_menu_name) { 'Packages & Registries' }
let_it_be(:package_entry_name) { 'Package Registry' }
diff --git a/spec/views/layouts/nav/sidebar/_project_security_link.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project_security_link.html.haml_spec.rb
new file mode 100644
index 00000000000..d3fb35bff6d
--- /dev/null
+++ b/spec/views/layouts/nav/sidebar/_project_security_link.html.haml_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'layouts/nav/sidebar/_project_security_link' do
+ let_it_be_with_reload(:project) { create(:project) }
+ context 'on security configuration' do
+ before do
+ assign(:project, project)
+ allow(controller).to receive(:controller_name).and_return('configuration')
+ allow(controller).to receive(:controller_path).and_return('projects/security/configuration')
+ allow(controller).to receive(:action_name).and_return('show')
+ allow(view).to receive(:any_project_nav_tab?).and_return(true)
+ allow(view).to receive(:project_nav_tab?).and_return(true)
+ end
+
+ it 'activates Security & Compliance tab' do
+ render
+
+ expect(rendered).to have_css('li.active', text: 'Security & Compliance')
+ end
+
+ it 'activates Configuration sub tab' do
+ render
+
+ expect(rendered).to have_css('.sidebar-sub-level-items > li.active', text: 'Configuration')
+ end
+ end
+end
diff --git a/spec/views/layouts/profile.html.haml_spec.rb b/spec/views/layouts/profile.html.haml_spec.rb
new file mode 100644
index 00000000000..93f8a075209
--- /dev/null
+++ b/spec/views/layouts/profile.html.haml_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'layouts/profile' do
+ let(:user) { create(:user) }
+
+ before do
+ allow(view).to receive(:session).and_return({})
+ allow(view).to receive(:current_user).and_return(user)
+ allow(view).to receive(:current_user_mode).and_return(Gitlab::Auth::CurrentUserMode.new(user))
+ allow(view).to receive(:experiment_enabled?).and_return(false)
+ allow(view).to receive(:enable_search_settings).and_call_original
+ end
+
+ it 'calls enable_search_settings helper with a custom container class' do
+ render
+ expect(view).to have_received(:enable_search_settings)
+ .with({ locals: { container_class: 'gl-my-5' } })
+ end
+
+ context 'when search_settings_in_page feature flag is on' do
+ it 'displays the search settings entry point' do
+ render
+ expect(rendered).to include('js-search-settings-app')
+ end
+ end
+
+ context 'when search_settings_in_page feature flag is off' do
+ before do
+ stub_feature_flags(search_settings_in_page: false)
+ end
+
+ it 'does not display the search settings entry point' do
+ render
+ expect(rendered).not_to include('js-search-settings-app')
+ end
+ end
+end
diff --git a/spec/views/projects/_home_panel.html.haml_spec.rb b/spec/views/projects/_home_panel.html.haml_spec.rb
index 548dba7874a..cc0eb9919da 100644
--- a/spec/views/projects/_home_panel.html.haml_spec.rb
+++ b/spec/views/projects/_home_panel.html.haml_spec.rb
@@ -9,6 +9,7 @@ RSpec.describe 'projects/_home_panel' do
let(:project) { create(:project) }
before do
+ stub_feature_flags(vue_notification_dropdown: false)
assign(:project, project)
allow(view).to receive(:current_user).and_return(user)
diff --git a/spec/views/projects/empty.html.haml_spec.rb b/spec/views/projects/empty.html.haml_spec.rb
index de83722160e..6762dcd22d5 100644
--- a/spec/views/projects/empty.html.haml_spec.rb
+++ b/spec/views/projects/empty.html.haml_spec.rb
@@ -79,4 +79,41 @@ RSpec.describe 'projects/empty' do
it_behaves_like 'no invite member info'
end
end
+
+ context 'when rendering with the layout' do
+ subject(:render_page) { render template: 'projects/empty.html.haml', layout: 'layouts/project' }
+
+ describe 'invite team members' do
+ before do
+ allow(view).to receive(:session).and_return({})
+ allow(view).to receive(:current_user_mode).and_return(Gitlab::Auth::CurrentUserMode.new(user))
+ allow(view).to receive(:current_user).and_return(user)
+ allow(view).to receive(:experiment_enabled?).and_return(false)
+ end
+
+ context 'when invite team members is not available in sidebar' do
+ before do
+ allow(view).to receive(:can_invite_members_for_project?).and_return(false)
+ end
+
+ it 'does not display the js-invite-members-trigger' do
+ render_page
+
+ expect(rendered).not_to have_selector('.js-invite-members-trigger')
+ end
+ end
+
+ context 'when invite team members is available' do
+ before do
+ allow(view).to receive(:can_invite_members_for_project?).and_return(true)
+ end
+
+ it 'includes the div for js-invite-members-trigger' do
+ render_page
+
+ expect(rendered).to have_selector('.js-invite-members-trigger')
+ end
+ end
+ end
+ end
end
diff --git a/spec/views/projects/show.html.haml_spec.rb b/spec/views/projects/show.html.haml_spec.rb
new file mode 100644
index 00000000000..995e31e83af
--- /dev/null
+++ b/spec/views/projects/show.html.haml_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'projects/show.html.haml' do
+ let_it_be(:user) { build(:user) }
+ let_it_be(:project) { ProjectPresenter.new(create(:project, :repository), current_user: user) }
+
+ before do
+ assign(:project, project)
+ end
+
+ context 'when rendering with the layout' do
+ subject(:render_page) { render template: 'projects/show.html.haml', layout: 'layouts/project' }
+
+ describe 'invite team members' do
+ before do
+ allow(view).to receive(:event_filter_link)
+ allow(view).to receive(:session).and_return({})
+ allow(view).to receive(:current_user_mode).and_return(Gitlab::Auth::CurrentUserMode.new(user))
+ allow(view).to receive(:current_user).and_return(user)
+ allow(view).to receive(:experiment_enabled?).and_return(false)
+ allow(view).to receive(:add_page_startup_graphql_call)
+ end
+
+ context 'when invite team members is not available in sidebar' do
+ before do
+ allow(view).to receive(:can_invite_members_for_project?).and_return(false)
+ end
+
+ it 'does not display the js-invite-members-trigger' do
+ render_page
+
+ expect(rendered).not_to have_selector('.js-invite-members-trigger')
+ end
+ end
+
+ context 'when invite team members is available' do
+ before do
+ allow(view).to receive(:can_invite_members_for_project?).and_return(true)
+ end
+
+ it 'includes the div for js-invite-members-trigger' do
+ render_page
+
+ expect(rendered).to have_selector('.js-invite-members-trigger')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/views/projects/tree/_tree_row.html.haml_spec.rb b/spec/views/projects/tree/_tree_row.html.haml_spec.rb
deleted file mode 100644
index 43a37934afd..00000000000
--- a/spec/views/projects/tree/_tree_row.html.haml_spec.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'projects/tree/_tree_row' do
- let(:project) { create(:project, :repository) }
- let(:repository) { project.repository }
-
- # rubocop: disable Rails/FindBy
- # This is not ActiveRecord where..first
- let(:blob_item) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID, 'files/ruby').first }
- # rubocop: enable Rails/FindBy
-
- before do
- assign(:project, project)
- assign(:repository, repository)
- assign(:id, File.join('master', ''))
- assign(:lfs_blob_ids, [])
- end
-
- it 'renders blob item' do
- render_partial(blob_item)
-
- expect(rendered).to have_content(blob_item.name)
- expect(rendered).not_to have_selector('.label-lfs', text: 'LFS')
- end
-
- describe 'LFS blob' do
- before do
- assign(:lfs_blob_ids, [blob_item].map(&:id))
-
- render_partial(blob_item)
- end
-
- it 'renders LFS badge' do
- expect(rendered).to have_selector('.label-lfs', text: 'LFS')
- end
- end
-
- def render_partial(items)
- render partial: 'projects/tree/tree_row', collection: [items].flatten
- end
-end
diff --git a/spec/views/registrations/welcome/show.html.haml_spec.rb b/spec/views/registrations/welcome/show.html.haml_spec.rb
index bede52bed4b..f731594e9ee 100644
--- a/spec/views/registrations/welcome/show.html.haml_spec.rb
+++ b/spec/views/registrations/welcome/show.html.haml_spec.rb
@@ -13,7 +13,6 @@ RSpec.describe 'registrations/welcome/show' do
allow(view).to receive(:in_trial_flow?).and_return(false)
allow(view).to receive(:in_invitation_flow?).and_return(false)
allow(view).to receive(:in_oauth_flow?).and_return(false)
- allow(view).to receive(:experiment_enabled?).with(:onboarding_issues).and_return(false)
allow(Gitlab).to receive(:com?).and_return(false)
render
diff --git a/spec/views/search/_filter.html.haml_spec.rb b/spec/views/search/_filter.html.haml_spec.rb
deleted file mode 100644
index 868408f7beb..00000000000
--- a/spec/views/search/_filter.html.haml_spec.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'search/_filter' do
- context 'when the search page is opened' do
- it 'displays the correct elements' do
- render
-
- expect(rendered).to have_selector('label[for="dashboard_search_group"]')
- expect(rendered).to have_selector('input#js-search-group-dropdown')
-
- expect(rendered).to have_selector('label[for="dashboard_search_project"]')
- expect(rendered).to have_selector('input#js-search-project-dropdown')
- end
- end
-end
diff --git a/spec/views/search/_form.html.haml_spec.rb b/spec/views/search/_form.html.haml_spec.rb
deleted file mode 100644
index 073a39e4ed6..00000000000
--- a/spec/views/search/_form.html.haml_spec.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'search/_form' do
- context 'when the search page is opened' do
- it 'displays the correct elements' do
- render
-
- expect(rendered).to have_selector('.search-field-holder.form-group')
- expect(rendered).to have_selector('label[for="dashboard_search"]')
- end
- end
-end
diff --git a/spec/views/shared/ssh_keys/_key_details.html.haml_spec.rb b/spec/views/shared/ssh_keys/_key_details.html.haml_spec.rb
new file mode 100644
index 00000000000..400319a42b7
--- /dev/null
+++ b/spec/views/shared/ssh_keys/_key_details.html.haml_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe 'shared/ssh_keys/_key_delete.html.haml' do
+ context 'when the text parameter is used' do
+ it 'has text' do
+ render 'shared/ssh_keys/key_delete.html.haml', text: 'Button', html_class: '', button_data: ''
+
+ expect(rendered).to have_button('Button')
+ end
+ end
+
+ context 'when the text parameter is not used' do
+ it 'does not have text' do
+ render 'shared/ssh_keys/key_delete.html.haml', html_class: '', button_data: ''
+
+ expect(rendered).to have_button('Delete')
+ end
+ end
+end
diff --git a/spec/workers/build_coverage_worker_spec.rb b/spec/workers/build_coverage_worker_spec.rb
deleted file mode 100644
index 4beb6e5972f..00000000000
--- a/spec/workers/build_coverage_worker_spec.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe BuildCoverageWorker do
- describe '#perform' do
- context 'when build exists' do
- let!(:build) { create(:ci_build) }
-
- it 'updates code coverage' do
- expect_any_instance_of(Ci::Build)
- .to receive(:update_coverage)
-
- described_class.new.perform(build.id)
- end
- end
-
- context 'when build does not exist' do
- it 'does not raise exception' do
- expect { described_class.new.perform(123) }
- .not_to raise_error
- end
- end
- end
-end
diff --git a/spec/workers/build_finished_worker_spec.rb b/spec/workers/build_finished_worker_spec.rb
index 02e15b7bc22..6d040f83dc7 100644
--- a/spec/workers/build_finished_worker_spec.rb
+++ b/spec/workers/build_finished_worker_spec.rb
@@ -11,20 +11,16 @@ RSpec.describe BuildFinishedWorker do
context 'when build exists' do
let!(:build) { create(:ci_build) }
- it 'calculates coverage and calls hooks', :aggregate_failures do
- trace_worker = double('trace worker')
- coverage_worker = double('coverage worker')
-
- allow(BuildTraceSectionsWorker).to receive(:new).and_return(trace_worker)
- allow(BuildCoverageWorker).to receive(:new).and_return(coverage_worker)
+ before do
+ expect(Ci::Build).to receive(:find_by).with(id: build.id).and_return(build)
+ end
- # Unfortunately, `ordered` does not seem to work when called within `allow_next_instance_of`
- # so we're doing this the long and dirty way
- expect(trace_worker).to receive(:perform).ordered
- expect(coverage_worker).to receive(:perform).ordered
+ it 'calculates coverage and calls hooks', :aggregate_failures do
+ expect(build).to receive(:parse_trace_sections!).ordered
+ expect(build).to receive(:update_coverage).ordered
- expect_next_instance_of(Ci::BuildReportResultWorker) do |instance|
- expect(instance).to receive(:perform)
+ expect_next_instance_of(Ci::BuildReportResultService) do |build_report_result_service|
+ expect(build_report_result_service).to receive(:execute).with(build)
end
expect(BuildHooksWorker).to receive(:perform_async)
diff --git a/spec/workers/build_trace_sections_worker_spec.rb b/spec/workers/build_trace_sections_worker_spec.rb
deleted file mode 100644
index 1c84b0083fb..00000000000
--- a/spec/workers/build_trace_sections_worker_spec.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe BuildTraceSectionsWorker do
- describe '#perform' do
- context 'when build exists' do
- let!(:build) { create(:ci_build) }
-
- it 'updates trace sections' do
- expect_any_instance_of(Ci::Build)
- .to receive(:parse_trace_sections!)
-
- described_class.new.perform(build.id)
- end
- end
-
- context 'when build does not exist' do
- it 'does not raise exception' do
- expect { described_class.new.perform(123) }
- .not_to raise_error
- end
- end
- end
-end
diff --git a/spec/workers/bulk_import_worker_spec.rb b/spec/workers/bulk_import_worker_spec.rb
index d3a4144d606..8cf14ed6f8b 100644
--- a/spec/workers/bulk_import_worker_spec.rb
+++ b/spec/workers/bulk_import_worker_spec.rb
@@ -72,6 +72,21 @@ RSpec.describe BulkImportWorker do
expect(bulk_import.entities.map(&:status_name)).to contain_exactly(:created, :started)
end
end
+
+ context 'when exception occurs' do
+ it 'tracks the exception & marks import as failed' do
+ bulk_import = create(:bulk_import, :created)
+ create(:bulk_import_entity, :created, bulk_import: bulk_import)
+
+ allow(BulkImports::EntityWorker).to receive(:perform_async).and_raise(StandardError)
+
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(kind_of(StandardError), bulk_import_id: bulk_import.id)
+
+ subject.perform(bulk_import.id)
+
+ expect(bulk_import.reload.failed?).to eq(true)
+ end
+ end
end
end
end
diff --git a/spec/workers/bulk_imports/entity_worker_spec.rb b/spec/workers/bulk_imports/entity_worker_spec.rb
index 31515b31947..cd9a6f605b9 100644
--- a/spec/workers/bulk_imports/entity_worker_spec.rb
+++ b/spec/workers/bulk_imports/entity_worker_spec.rb
@@ -24,6 +24,20 @@ RSpec.describe BulkImports::EntityWorker do
expect(entity.reload.jid).to eq(jid)
end
+
+ context 'when exception occurs' do
+ it 'tracks the exception & marks entity as failed' do
+ allow(BulkImports::Importers::GroupImporter).to receive(:new) { raise StandardError }
+
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_exception)
+ .with(kind_of(StandardError), bulk_import_id: bulk_import.id, entity_id: entity.id)
+
+ subject.perform(entity.id)
+
+ expect(entity.reload.failed?).to eq(true)
+ end
+ end
end
context 'when started entity does not exist' do
diff --git a/spec/workers/ci/build_report_result_worker_spec.rb b/spec/workers/ci/build_report_result_worker_spec.rb
deleted file mode 100644
index aedfa70c8b8..00000000000
--- a/spec/workers/ci/build_report_result_worker_spec.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Ci::BuildReportResultWorker do
- subject { described_class.new.perform(build_id) }
-
- context 'when build exists' do
- let(:build) { create(:ci_build) }
- let(:build_id) { build.id }
-
- it 'calls build report result service' do
- expect_next_instance_of(Ci::BuildReportResultService) do |build_report_result_service|
- expect(build_report_result_service).to receive(:execute)
- end
-
- subject
- end
- end
-
- context 'when build does not exist' do
- let(:build_id) { -1 }
-
- it 'does not call build report result service' do
- expect(Ci::BuildReportResultService).not_to receive(:execute)
-
- subject
- end
- end
-end
diff --git a/spec/workers/ci/pipeline_artifacts/create_quality_report_worker_spec.rb b/spec/workers/ci/pipeline_artifacts/create_quality_report_worker_spec.rb
new file mode 100644
index 00000000000..be351032b58
--- /dev/null
+++ b/spec/workers/ci/pipeline_artifacts/create_quality_report_worker_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Ci::PipelineArtifacts::CreateQualityReportWorker do
+ describe '#perform' do
+ subject { described_class.new.perform(pipeline_id) }
+
+ context 'when pipeline exists' do
+ let(:pipeline) { create(:ci_pipeline, :with_codequality_reports) }
+ let(:pipeline_id) { pipeline.id }
+
+ it 'calls pipeline codequality report service' do
+ expect_next_instance_of(::Ci::PipelineArtifacts::CreateCodeQualityMrDiffReportService) do |quality_report_service|
+ expect(quality_report_service).to receive(:execute)
+ end
+
+ subject
+ end
+
+ it_behaves_like 'an idempotent worker' do
+ let(:job_args) { pipeline_id }
+
+ it 'creates a pipeline artifact' do
+ expect { subject }.to change { pipeline.pipeline_artifacts.count }.by(1)
+ end
+ end
+ end
+
+ context 'when pipeline does not exist' do
+ let(:pipeline_id) { non_existing_record_id }
+
+ it 'does not call pipeline codequality report service' do
+ expect(Ci::PipelineArtifacts::CreateCodeQualityMrDiffReportService).not_to receive(:execute)
+
+ subject
+ end
+ end
+ end
+end
diff --git a/spec/workers/ci/pipeline_artifacts/expire_artifacts_worker_spec.rb b/spec/workers/ci/pipeline_artifacts/expire_artifacts_worker_spec.rb
index 9e9aa962b63..2bdd8345374 100644
--- a/spec/workers/ci/pipeline_artifacts/expire_artifacts_worker_spec.rb
+++ b/spec/workers/ci/pipeline_artifacts/expire_artifacts_worker_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Ci::PipelineArtifacts::ExpireArtifactsWorker do
describe '#perform' do
let_it_be(:pipeline_artifact) do
- create(:ci_pipeline_artifact, expire_at: 1.week.ago)
+ create(:ci_pipeline_artifact, :with_coverage_report, expire_at: 1.week.ago)
end
it 'executes a service' do
diff --git a/spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb b/spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb
index e6592f7f204..eb4faaed769 100644
--- a/spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb
+++ b/spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb
@@ -82,6 +82,7 @@ RSpec.describe ContainerExpirationPolicies::CleanupContainerRepositoryWorker do
it 'skips the repository' do
expect(ContainerExpirationPolicies::CleanupService).not_to receive(:new)
expect(worker).to receive(:log_extra_metadata_on_done).with(:container_repository_id, repository.id)
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:project_id, repository.project.id)
expect(worker).to receive(:log_extra_metadata_on_done).with(:cleanup_status, :skipped)
expect { subject }.to change { ContainerRepository.waiting_for_cleanup.count }.from(1).to(0)
@@ -198,7 +199,7 @@ RSpec.describe ContainerExpirationPolicies::CleanupContainerRepositoryWorker do
end
end
- def cleanup_service_response(status: :finished, repository:, cleanup_tags_service_original_size: 100, cleanup_tags_service_before_truncate_size: 80, cleanup_tags_service_after_truncate_size: 80, cleanup_tags_service_before_delete_size: 50)
+ def cleanup_service_response(status: :finished, repository:, cleanup_tags_service_original_size: 100, cleanup_tags_service_before_truncate_size: 80, cleanup_tags_service_after_truncate_size: 80, cleanup_tags_service_before_delete_size: 50, cleanup_tags_service_deleted_size: 50)
ServiceResponse.success(
message: "cleanup #{status}",
payload: {
@@ -213,13 +214,16 @@ RSpec.describe ContainerExpirationPolicies::CleanupContainerRepositoryWorker do
end
def expect_log_extra_metadata(service_response:, cleanup_status: :finished, truncated: false)
- expect(worker).to receive(:log_extra_metadata_on_done).with(:cleanup_status, cleanup_status)
expect(worker).to receive(:log_extra_metadata_on_done).with(:container_repository_id, repository.id)
- %i[cleanup_tags_service_original_size cleanup_tags_service_before_truncate_size cleanup_tags_service_after_truncate_size cleanup_tags_service_before_delete_size].each do |field|
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:project_id, repository.project.id)
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:cleanup_status, cleanup_status)
+
+ %i[cleanup_tags_service_original_size cleanup_tags_service_before_truncate_size cleanup_tags_service_after_truncate_size cleanup_tags_service_before_delete_size cleanup_tags_service_deleted_size].each do |field|
value = service_response.payload[field]
expect(worker).to receive(:log_extra_metadata_on_done).with(field, value) unless value.nil?
end
expect(worker).to receive(:log_extra_metadata_on_done).with(:cleanup_tags_service_truncated, truncated)
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:running_jobs_count, 0)
end
end
diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb
index 13089549086..3df64c35166 100644
--- a/spec/workers/git_garbage_collect_worker_spec.rb
+++ b/spec/workers/git_garbage_collect_worker_spec.rb
@@ -5,350 +5,22 @@ require 'fileutils'
require 'spec_helper'
RSpec.describe GitGarbageCollectWorker do
- include GitHelpers
-
let_it_be(:project) { create(:project, :repository) }
- let(:shell) { Gitlab::Shell.new }
- let!(:lease_uuid) { SecureRandom.uuid }
- let!(:lease_key) { "project_housekeeping:#{project.id}" }
- let(:params) { [project.id, task, lease_key, lease_uuid] }
-
- subject { described_class.new }
-
- shared_examples 'it calls Gitaly' do
- specify do
- expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(gitaly_task)
- .and_return(nil)
-
- subject.perform(*params)
- end
- end
- shared_examples 'it updates the project statistics' do
- it 'updates the project statistics' do
- expect_next_instance_of(Projects::UpdateStatisticsService, project, nil, statistics: [:repository_size, :lfs_objects_size]) do |service|
- expect(service).to receive(:execute).and_call_original
- end
-
- subject.perform(*params)
- end
+ let(:lease_uuid) { SecureRandom.uuid }
+ let(:lease_key) { "project_housekeeping:#{project.id}" }
+ let(:task) { :full_repack }
+ let(:params) { [project.id, task, lease_key, lease_uuid] }
- it 'does nothing if the database is read-only' do
- allow(Gitlab::Database).to receive(:read_only?) { true }
-
- expect_any_instance_of(Projects::UpdateStatisticsService).not_to receive(:execute)
-
- subject.perform(*params)
- end
- end
+ subject { described_class.new }
describe "#perform" do
- let(:gitaly_task) { :garbage_collect }
- let(:task) { :gc }
-
- context 'with active lease_uuid' do
- before do
- allow(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
- end
-
- it_behaves_like 'it calls Gitaly'
- it_behaves_like 'it updates the project statistics'
-
- it "flushes ref caches when the task if 'gc'" do
- expect(subject).to receive(:renew_lease).with(lease_key, lease_uuid).and_call_original
- expect_any_instance_of(Repository).to receive(:expire_branches_cache).and_call_original
- expect_any_instance_of(Repository).to receive(:branch_names).and_call_original
- expect_any_instance_of(Repository).to receive(:has_visible_content?).and_call_original
- expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original
-
- subject.perform(*params)
- end
-
- it 'handles gRPC errors' do
- expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:garbage_collect).and_raise(GRPC::NotFound)
-
- expect { subject.perform(*params) }.to raise_exception(Gitlab::Git::Repository::NoRepository)
- end
- end
-
- context 'with different lease than the active one' do
- before do
- allow(subject).to receive(:get_lease_uuid).and_return(SecureRandom.uuid)
- end
-
- it 'returns silently' do
- expect_any_instance_of(Repository).not_to receive(:expire_branches_cache).and_call_original
- expect_any_instance_of(Repository).not_to receive(:branch_names).and_call_original
- expect_any_instance_of(Repository).not_to receive(:has_visible_content?).and_call_original
-
- subject.perform(*params)
- end
- end
-
- context 'with no active lease' do
- let(:params) { [project.id] }
-
- before do
- allow(subject).to receive(:get_lease_uuid).and_return(false)
- end
-
- context 'when is able to get the lease' do
- before do
- allow(subject).to receive(:try_obtain_lease).and_return(SecureRandom.uuid)
- end
-
- it_behaves_like 'it calls Gitaly'
- it_behaves_like 'it updates the project statistics'
-
- it "flushes ref caches when the task if 'gc'" do
- expect(subject).to receive(:get_lease_uuid).with("git_gc:#{task}:#{project.id}").and_return(false)
- expect_any_instance_of(Repository).to receive(:expire_branches_cache).and_call_original
- expect_any_instance_of(Repository).to receive(:branch_names).and_call_original
- expect_any_instance_of(Repository).to receive(:has_visible_content?).and_call_original
- expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original
-
- subject.perform(*params)
- end
-
- context 'when the repository has joined a pool' do
- let!(:pool) { create(:pool_repository, :ready) }
- let(:project) { pool.source_project }
-
- it 'ensures the repositories are linked' do
- expect_any_instance_of(PoolRepository).to receive(:link_repository).once
-
- subject.perform(*params)
- end
- end
-
- context 'LFS object garbage collection' do
- before do
- stub_lfs_setting(enabled: true)
- end
-
- let_it_be(:lfs_reference) { create(:lfs_objects_project, project: project) }
- let(:lfs_object) { lfs_reference.lfs_object }
-
- it 'cleans up unreferenced LFS objects' do
- expect_next_instance_of(Gitlab::Cleanup::OrphanLfsFileReferences) do |svc|
- expect(svc.project).to eq(project)
- expect(svc.dry_run).to be_falsy
- expect(svc).to receive(:run!).and_call_original
- end
-
- subject.perform(*params)
-
- expect(project.lfs_objects.reload).not_to include(lfs_object)
- end
-
- it 'catches and logs exceptions' do
- expect_any_instance_of(Gitlab::Cleanup::OrphanLfsFileReferences)
- .to receive(:run!)
- .and_raise(/Failed/)
-
- expect(Gitlab::GitLogger).to receive(:warn)
- expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
-
- subject.perform(*params)
- end
-
- it 'does nothing if the database is read-only' do
- allow(Gitlab::Database).to receive(:read_only?) { true }
- expect_any_instance_of(Gitlab::Cleanup::OrphanLfsFileReferences).not_to receive(:run!)
-
- subject.perform(*params)
-
- expect(project.lfs_objects.reload).to include(lfs_object)
- end
- end
- end
-
- context 'when no lease can be obtained' do
- before do
- expect(subject).to receive(:try_obtain_lease).and_return(false)
- end
-
- it 'returns silently' do
- expect(subject).not_to receive(:command)
- expect_any_instance_of(Repository).not_to receive(:expire_branches_cache).and_call_original
- expect_any_instance_of(Repository).not_to receive(:branch_names).and_call_original
- expect_any_instance_of(Repository).not_to receive(:has_visible_content?).and_call_original
-
- subject.perform(*params)
- end
- end
- end
-
- context "repack_full" do
- let(:task) { :full_repack }
- let(:gitaly_task) { :repack_full }
-
- before do
- expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
+ it 'calls the Projects::GitGarbageGitGarbageCollectWorker with the same params' do
+ expect_next_instance_of(Projects::GitGarbageCollectWorker) do |instance|
+ expect(instance).to receive(:perform).with(*params)
end
- it_behaves_like 'it calls Gitaly'
- it_behaves_like 'it updates the project statistics'
- end
-
- context "pack_refs" do
- let(:task) { :pack_refs }
- let(:gitaly_task) { :pack_refs }
-
- before do
- expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
- end
-
- it "calls Gitaly" do
- expect_any_instance_of(Gitlab::GitalyClient::RefService).to receive(task)
- .and_return(nil)
-
- subject.perform(*params)
- end
-
- it 'does not update the project statistics' do
- expect(Projects::UpdateStatisticsService).not_to receive(:new)
-
- subject.perform(*params)
- end
- end
-
- context "repack_incremental" do
- let(:task) { :incremental_repack }
- let(:gitaly_task) { :repack_incremental }
-
- before do
- expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
- end
-
- it_behaves_like 'it calls Gitaly'
- it_behaves_like 'it updates the project statistics'
- end
-
- shared_examples 'gc tasks' do
- before do
- allow(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
- allow(subject).to receive(:bitmaps_enabled?).and_return(bitmaps_enabled)
- end
-
- it 'incremental repack adds a new packfile' do
- create_objects(project)
- before_packs = packs(project)
-
- expect(before_packs.count).to be >= 1
-
- subject.perform(project.id, 'incremental_repack', lease_key, lease_uuid)
- after_packs = packs(project)
-
- # Exactly one new pack should have been created
- expect(after_packs.count).to eq(before_packs.count + 1)
-
- # Previously existing packs are still around
- expect(before_packs & after_packs).to eq(before_packs)
- end
-
- it 'full repack consolidates into 1 packfile' do
- create_objects(project)
- subject.perform(project.id, 'incremental_repack', lease_key, lease_uuid)
- before_packs = packs(project)
-
- expect(before_packs.count).to be >= 2
-
- subject.perform(project.id, 'full_repack', lease_key, lease_uuid)
- after_packs = packs(project)
-
- expect(after_packs.count).to eq(1)
-
- # Previously existing packs should be gone now
- expect(after_packs - before_packs).to eq(after_packs)
-
- expect(File.exist?(bitmap_path(after_packs.first))).to eq(bitmaps_enabled)
- end
-
- it 'gc consolidates into 1 packfile and updates packed-refs' do
- create_objects(project)
- before_packs = packs(project)
- before_packed_refs = packed_refs(project)
-
- expect(before_packs.count).to be >= 1
-
- expect_any_instance_of(Gitlab::GitalyClient::RepositoryService)
- .to receive(:garbage_collect)
- .with(bitmaps_enabled, prune: false)
- .and_call_original
-
- subject.perform(project.id, 'gc', lease_key, lease_uuid)
- after_packed_refs = packed_refs(project)
- after_packs = packs(project)
-
- expect(after_packs.count).to eq(1)
-
- # Previously existing packs should be gone now
- expect(after_packs - before_packs).to eq(after_packs)
-
- # The packed-refs file should have been updated during 'git gc'
- expect(before_packed_refs).not_to eq(after_packed_refs)
-
- expect(File.exist?(bitmap_path(after_packs.first))).to eq(bitmaps_enabled)
- end
-
- it 'cleans up repository after finishing' do
- expect_any_instance_of(Project).to receive(:cleanup).and_call_original
-
- subject.perform(project.id, 'gc', lease_key, lease_uuid)
- end
-
- it 'prune calls garbage_collect with the option prune: true' do
- expect_any_instance_of(Gitlab::GitalyClient::RepositoryService)
- .to receive(:garbage_collect)
- .with(bitmaps_enabled, prune: true)
- .and_return(nil)
-
- subject.perform(project.id, 'prune', lease_key, lease_uuid)
- end
- end
-
- context 'with bitmaps enabled' do
- let(:bitmaps_enabled) { true }
-
- include_examples 'gc tasks'
- end
-
- context 'with bitmaps disabled' do
- let(:bitmaps_enabled) { false }
-
- include_examples 'gc tasks'
- end
- end
-
- # Create a new commit on a random new branch
- def create_objects(project)
- rugged = rugged_repo(project.repository)
- old_commit = rugged.branches.first.target
- new_commit_sha = Rugged::Commit.create(
- rugged,
- message: "hello world #{SecureRandom.hex(6)}",
- author: { email: 'foo@bar', name: 'baz' },
- committer: { email: 'foo@bar', name: 'baz' },
- tree: old_commit.tree,
- parents: [old_commit]
- )
- rugged.references.create("refs/heads/#{SecureRandom.hex(6)}", new_commit_sha)
- end
-
- def packs(project)
- Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- Dir["#{project.repository.path_to_repo}/objects/pack/*.pack"]
+ subject.perform(*params)
end
end
-
- def packed_refs(project)
- path = "#{project.repository.path_to_repo}/packed-refs"
- FileUtils.touch(path)
- File.read(path)
- end
-
- def bitmap_path(pack)
- pack.sub(/\.pack\z/, '.bitmap')
- end
end
diff --git a/spec/workers/jira_connect/sync_builds_worker_spec.rb b/spec/workers/jira_connect/sync_builds_worker_spec.rb
index 7c58803d778..8fb8692fdf7 100644
--- a/spec/workers/jira_connect/sync_builds_worker_spec.rb
+++ b/spec/workers/jira_connect/sync_builds_worker_spec.rb
@@ -32,29 +32,5 @@ RSpec.describe ::JiraConnect::SyncBuildsWorker do
subject
end
end
-
- context 'when the feature flag is disabled' do
- before do
- stub_feature_flags(jira_sync_builds: false)
- end
-
- it 'does not call the sync service' do
- expect_next(::JiraConnect::SyncService).not_to receive(:execute)
-
- subject
- end
- end
-
- context 'when the feature flag is enabled for this project' do
- before do
- stub_feature_flags(jira_sync_builds: pipeline.project)
- end
-
- it 'calls the sync service' do
- expect_next(::JiraConnect::SyncService).to receive(:execute)
-
- subject
- end
- end
end
end
diff --git a/spec/workers/jira_connect/sync_deployments_worker_spec.rb b/spec/workers/jira_connect/sync_deployments_worker_spec.rb
index 9485f4cd3a7..16fa2643d04 100644
--- a/spec/workers/jira_connect/sync_deployments_worker_spec.rb
+++ b/spec/workers/jira_connect/sync_deployments_worker_spec.rb
@@ -32,29 +32,5 @@ RSpec.describe ::JiraConnect::SyncDeploymentsWorker do
subject
end
end
-
- context 'when the feature flag is disabled' do
- before do
- stub_feature_flags(jira_sync_deployments: false)
- end
-
- it 'does not call the sync service' do
- expect_next(::JiraConnect::SyncService).not_to receive(:execute)
-
- subject
- end
- end
-
- context 'when the feature flag is enabled for this project' do
- before do
- stub_feature_flags(jira_sync_deployments: deployment.project)
- end
-
- it 'calls the sync service' do
- expect_next(::JiraConnect::SyncService).to receive(:execute)
-
- subject
- end
- end
end
end
diff --git a/spec/workers/jira_connect/sync_feature_flags_worker_spec.rb b/spec/workers/jira_connect/sync_feature_flags_worker_spec.rb
index 035f4ebdd3c..038eed7b9f1 100644
--- a/spec/workers/jira_connect/sync_feature_flags_worker_spec.rb
+++ b/spec/workers/jira_connect/sync_feature_flags_worker_spec.rb
@@ -32,29 +32,5 @@ RSpec.describe ::JiraConnect::SyncFeatureFlagsWorker do
subject
end
end
-
- context 'when the feature flag is disabled' do
- before do
- stub_feature_flags(jira_sync_feature_flags: false)
- end
-
- it 'does not call the sync service' do
- expect_next(::JiraConnect::SyncService).not_to receive(:execute)
-
- subject
- end
- end
-
- context 'when the feature flag is enabled for this project' do
- before do
- stub_feature_flags(jira_sync_feature_flags: feature_flag.project)
- end
-
- it 'calls the sync service' do
- expect_next(::JiraConnect::SyncService).to receive(:execute)
-
- subject
- end
- end
end
end
diff --git a/spec/workers/merge_request_cleanup_refs_worker_spec.rb b/spec/workers/merge_request_cleanup_refs_worker_spec.rb
index 88d7322536b..7401c6dd4d7 100644
--- a/spec/workers/merge_request_cleanup_refs_worker_spec.rb
+++ b/spec/workers/merge_request_cleanup_refs_worker_spec.rb
@@ -17,6 +17,18 @@ RSpec.describe MergeRequestCleanupRefsWorker do
subject
end
end
+
+ context 'when merge_request_refs_cleanup flag is disabled' do
+ before do
+ stub_feature_flags(merge_request_refs_cleanup: false)
+ end
+
+ it 'does not clean up the merge request' do
+ expect(MergeRequests::CleanupRefsService).not_to receive(:new)
+
+ perform_multiple(1)
+ end
+ end
end
context 'when merge request does not exist' do
diff --git a/spec/workers/namespaceless_project_destroy_worker_spec.rb b/spec/workers/namespaceless_project_destroy_worker_spec.rb
index 618cd9cabe9..cd66af82364 100644
--- a/spec/workers/namespaceless_project_destroy_worker_spec.rb
+++ b/spec/workers/namespaceless_project_destroy_worker_spec.rb
@@ -49,7 +49,7 @@ RSpec.describe NamespacelessProjectDestroyWorker do
subject.perform(project.id)
end
- it 'does not do anything in Project#remove_pages method' do
+ it 'does not do anything in Project#legacy_remove_pages method' do
expect(Gitlab::PagesTransfer).not_to receive(:new)
subject.perform(project.id)
diff --git a/spec/workers/namespaces/in_product_marketing_emails_worker_spec.rb b/spec/workers/namespaces/in_product_marketing_emails_worker_spec.rb
new file mode 100644
index 00000000000..722ecfc1dec
--- /dev/null
+++ b/spec/workers/namespaces/in_product_marketing_emails_worker_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Namespaces::InProductMarketingEmailsWorker, '#perform' do
+ context 'when the experiment is inactive' do
+ before do
+ stub_experiment(in_product_marketing_emails: false)
+ end
+
+ it 'does not execute the in product marketing emails service' do
+ expect(Namespaces::InProductMarketingEmailsService).not_to receive(:send_for_all_tracks_and_intervals)
+
+ subject.perform
+ end
+ end
+
+ context 'when the experiment is active' do
+ before do
+ stub_experiment(in_product_marketing_emails: true)
+ end
+
+ it 'calls the send_for_all_tracks_and_intervals method on the in product marketing emails service' do
+ expect(Namespaces::InProductMarketingEmailsService).to receive(:send_for_all_tracks_and_intervals)
+
+ subject.perform
+ end
+ end
+end
diff --git a/spec/workers/namespaces/onboarding_pipeline_created_worker_spec.rb b/spec/workers/namespaces/onboarding_pipeline_created_worker_spec.rb
index f1789fa8fbd..6d69ccb50bd 100644
--- a/spec/workers/namespaces/onboarding_pipeline_created_worker_spec.rb
+++ b/spec/workers/namespaces/onboarding_pipeline_created_worker_spec.rb
@@ -3,30 +3,15 @@
require 'spec_helper'
RSpec.describe Namespaces::OnboardingPipelineCreatedWorker, '#perform' do
- include AfterNextHelpers
-
let_it_be(:ci_pipeline) { create(:ci_pipeline) }
- before do
- OnboardingProgress.onboard(ci_pipeline.project.namespace)
- end
-
- it 'registers an onboarding progress action' do
- expect_next(OnboardingProgressService, ci_pipeline.project.namespace)
- .to receive(:execute).with(action: :pipeline_created).and_call_original
+ it_behaves_like 'records an onboarding progress action', :pipeline_created do
+ let(:namespace) { ci_pipeline.project.namespace }
- subject.perform(ci_pipeline.project.namespace_id)
-
- expect(OnboardingProgress.completed?(ci_pipeline.project.namespace, :pipeline_created)).to eq(true)
+ subject { described_class.new.perform(ci_pipeline.project.namespace_id) }
end
- context "when a namespace doesn't exist" do
- it 'does not register an onboarding progress action' do
- expect_next(OnboardingProgressService, ci_pipeline.project.namespace).not_to receive(:execute)
-
- subject.perform(nil)
-
- expect(OnboardingProgress.completed?(ci_pipeline.project.namespace, :pipeline_created)).to eq(false)
- end
+ it_behaves_like 'does not record an onboarding progress action' do
+ subject { described_class.new.perform(nil) }
end
end
diff --git a/spec/workers/namespaces/onboarding_user_added_worker_spec.rb b/spec/workers/namespaces/onboarding_user_added_worker_spec.rb
index a773e160fab..14428c0ecb8 100644
--- a/spec/workers/namespaces/onboarding_user_added_worker_spec.rb
+++ b/spec/workers/namespaces/onboarding_user_added_worker_spec.rb
@@ -3,20 +3,9 @@
require 'spec_helper'
RSpec.describe Namespaces::OnboardingUserAddedWorker, '#perform' do
- include AfterNextHelpers
+ let_it_be(:namespace) { create(:group) }
- let_it_be(:group) { create(:group) }
+ subject { described_class.new.perform(namespace.id) }
- before do
- OnboardingProgress.onboard(group)
- end
-
- it 'registers an onboarding progress action' do
- expect_next(OnboardingProgressService, group)
- .to receive(:execute).with(action: :user_added).and_call_original
-
- subject.perform(group.id)
-
- expect(OnboardingProgress.completed?(group, :user_added)).to be(true)
- end
+ it_behaves_like 'records an onboarding progress action', :user_added
end
diff --git a/spec/workers/new_note_worker_spec.rb b/spec/workers/new_note_worker_spec.rb
index 86b6d041e5c..7ba3fe94254 100644
--- a/spec/workers/new_note_worker_spec.rb
+++ b/spec/workers/new_note_worker_spec.rb
@@ -65,4 +65,24 @@ RSpec.describe NewNoteWorker do
subject.perform(note.id)
end
end
+
+ context 'when Note author has been blocked' do
+ let_it_be(:note) { create(:note, author: create(:user, :blocked)) }
+
+ it "does not call NotificationService" do
+ expect(NotificationService).not_to receive(:new)
+
+ described_class.new.perform(note.id)
+ end
+ end
+
+ context 'when Note author has been deleted' do
+ let_it_be(:note) { create(:note, author: User.ghost) }
+
+ it "does not call NotificationService" do
+ expect(NotificationService).not_to receive(:new)
+
+ described_class.new.perform(note.id)
+ end
+ end
end
diff --git a/spec/workers/packages/composer/cache_cleanup_worker_spec.rb b/spec/workers/packages/composer/cache_cleanup_worker_spec.rb
new file mode 100644
index 00000000000..e69fe55acc2
--- /dev/null
+++ b/spec/workers/packages/composer/cache_cleanup_worker_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Composer::CacheCleanupWorker, type: :worker do
+ describe '#perform' do
+ let_it_be(:group) { create(:group) }
+
+ let!(:cache_file1) { create(:composer_cache_file, delete_at: nil, group: group, file_sha256: '124') }
+ let!(:cache_file2) { create(:composer_cache_file, delete_at: 2.days.from_now, group: group, file_sha256: '3456') }
+ let!(:cache_file3) { create(:composer_cache_file, delete_at: 1.day.ago, group: group, file_sha256: '5346') }
+ let!(:cache_file4) { create(:composer_cache_file, delete_at: nil, group: group, file_sha256: '56889') }
+
+ subject { described_class.new.perform }
+
+ before do
+ # emulate group deletion
+ cache_file4.update_columns(namespace_id: nil)
+ end
+
+ it 'deletes expired packages' do
+ expect { subject }.to change { Packages::Composer::CacheFile.count }.by(-2)
+ expect { cache_file1.reload }.not_to raise_error ActiveRecord::RecordNotFound
+ expect { cache_file2.reload }.not_to raise_error ActiveRecord::RecordNotFound
+ expect { cache_file3.reload }.to raise_error ActiveRecord::RecordNotFound
+ expect { cache_file4.reload }.to raise_error ActiveRecord::RecordNotFound
+ end
+ end
+end
diff --git a/spec/workers/pages_remove_worker_spec.rb b/spec/workers/pages_remove_worker_spec.rb
index 638e87043f2..864aa763fa9 100644
--- a/spec/workers/pages_remove_worker_spec.rb
+++ b/spec/workers/pages_remove_worker_spec.rb
@@ -3,24 +3,23 @@
require 'spec_helper'
RSpec.describe PagesRemoveWorker do
- let_it_be(:project) { create(:project, path: "my.project")}
- let_it_be(:domain) { create(:pages_domain, project: project) }
+ let(:project) { create(:project, path: "my.project")}
+ let!(:domain) { create(:pages_domain, project: project) }
+
subject { described_class.new.perform(project.id) }
+ before do
+ project.mark_pages_as_deployed
+ end
+
it 'deletes published pages' do
+ expect(project.pages_deployed?).to be(true)
+
expect_any_instance_of(Gitlab::PagesTransfer).to receive(:rename_project).and_return true
expect(PagesWorker).to receive(:perform_in).with(5.minutes, :remove, project.namespace.full_path, anything)
subject
- expect(project.reload.pages_metadatum.deployed?).to be(false)
- end
-
- it 'deletes all domains' do
- expect(project.pages_domains.count).to be 1
-
- subject
-
- expect(project.reload.pages_domains.count).to be 0
+ expect(project.reload.pages_deployed?).to be(false)
end
end
diff --git a/spec/workers/pages_transfer_worker_spec.rb b/spec/workers/pages_transfer_worker_spec.rb
index 248a3713bf6..7d17461bc5a 100644
--- a/spec/workers/pages_transfer_worker_spec.rb
+++ b/spec/workers/pages_transfer_worker_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe PagesTransferWorker do
describe '#perform' do
- Gitlab::PagesTransfer::Async::METHODS.each do |meth|
+ Gitlab::PagesTransfer::METHODS.each do |meth|
context "when method is #{meth}" do
let(:args) { [1, 2, 3] }
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 5b8d8878a99..aaae0988602 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -85,6 +85,14 @@ RSpec.describe PostReceive do
perform
end
+
+ it 'tracks an event for the new_project_readme experiment', :experiment do
+ expect_next_instance_of(NewProjectReadmeExperiment, :new_project_readme, nil, actor: empty_project.owner) do |e|
+ expect(e).to receive(:track_initial_writes).with(empty_project)
+ end
+
+ perform
+ end
end
shared_examples 'not updating remote mirrors' do
diff --git a/spec/workers/projects/git_garbage_collect_worker_spec.rb b/spec/workers/projects/git_garbage_collect_worker_spec.rb
new file mode 100644
index 00000000000..8c44643ae51
--- /dev/null
+++ b/spec/workers/projects/git_garbage_collect_worker_spec.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::GitGarbageCollectWorker do
+ let_it_be(:project) { create(:project, :repository) }
+
+ it_behaves_like 'can collect git garbage' do
+ let(:resource) { project }
+ let(:statistics_service_klass) { Projects::UpdateStatisticsService }
+ let(:statistics_keys) { [:repository_size, :lfs_objects_size] }
+ let(:expected_default_lease) { "projects:#{resource.id}" }
+ end
+
+ context 'when is able to get the lease' do
+ let(:params) { [project.id] }
+
+ subject { described_class.new }
+
+ before do
+ allow(subject).to receive(:get_lease_uuid).and_return(false)
+ allow(subject).to receive(:find_resource).and_return(project)
+ allow(subject).to receive(:try_obtain_lease).and_return(SecureRandom.uuid)
+ end
+
+ context 'when the repository has joined a pool' do
+ let!(:pool) { create(:pool_repository, :ready) }
+ let(:project) { pool.source_project }
+
+ it 'ensures the repositories are linked' do
+ expect(project.pool_repository).to receive(:link_repository).once
+
+ subject.perform(*params)
+ end
+ end
+
+ context 'LFS object garbage collection' do
+ let_it_be(:lfs_reference) { create(:lfs_objects_project, project: project) }
+ let(:lfs_object) { lfs_reference.lfs_object }
+
+ before do
+ stub_lfs_setting(enabled: true)
+ end
+
+ it 'cleans up unreferenced LFS objects' do
+ expect_next_instance_of(Gitlab::Cleanup::OrphanLfsFileReferences) do |svc|
+ expect(svc.project).to eq(project)
+ expect(svc.dry_run).to be_falsy
+ expect(svc).to receive(:run!).and_call_original
+ end
+
+ subject.perform(*params)
+
+ expect(project.lfs_objects.reload).not_to include(lfs_object)
+ end
+
+ it 'catches and logs exceptions' do
+ allow_next_instance_of(Gitlab::Cleanup::OrphanLfsFileReferences) do |svc|
+ allow(svg).to receive(:run!).and_raise(/Failed/)
+ end
+
+ expect(Gitlab::GitLogger).to receive(:warn)
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
+
+ subject.perform(*params)
+ end
+
+ it 'does nothing if the database is read-only' do
+ allow(Gitlab::Database).to receive(:read_only?) { true }
+ expect(Gitlab::Cleanup::OrphanLfsFileReferences).not_to receive(:new)
+
+ subject.perform(*params)
+
+ expect(project.lfs_objects.reload).to include(lfs_object)
+ end
+ end
+ end
+end
diff --git a/spec/workers/schedule_merge_request_cleanup_refs_worker_spec.rb b/spec/workers/schedule_merge_request_cleanup_refs_worker_spec.rb
index 0dd50efba1c..869818b257e 100644
--- a/spec/workers/schedule_merge_request_cleanup_refs_worker_spec.rb
+++ b/spec/workers/schedule_merge_request_cleanup_refs_worker_spec.rb
@@ -20,6 +20,18 @@ RSpec.describe ScheduleMergeRequestCleanupRefsWorker do
worker.perform
end
+ context 'when merge_request_refs_cleanup flag is disabled' do
+ before do
+ stub_feature_flags(merge_request_refs_cleanup: false)
+ end
+
+ it 'does not schedule any merge request clean ups' do
+ expect(MergeRequestCleanupRefsWorker).not_to receive(:bulk_perform_in)
+
+ worker.perform
+ end
+ end
+
include_examples 'an idempotent worker' do
it 'schedules MergeRequestCleanupRefsWorker to be performed by batch' do
expect(MergeRequestCleanupRefsWorker)
diff --git a/spec/workers/user_status_cleanup/batch_worker_spec.rb b/spec/workers/user_status_cleanup/batch_worker_spec.rb
new file mode 100644
index 00000000000..2fd84d0e085
--- /dev/null
+++ b/spec/workers/user_status_cleanup/batch_worker_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe UserStatusCleanup::BatchWorker do
+ include_examples 'an idempotent worker' do
+ subject do
+ perform_multiple([], worker: described_class.new)
+ end
+ end
+
+ describe '#perform' do
+ subject(:run_worker) { described_class.new.perform }
+
+ context 'when no records are scheduled for cleanup' do
+ let(:user_status) { create(:user_status) }
+
+ it 'does nothing' do
+ expect { run_worker }.not_to change { user_status.reload }
+ end
+ end
+
+ it 'cleans up the records' do
+ user_status_1 = create(:user_status, clear_status_at: 1.year.ago)
+ user_status_2 = create(:user_status, clear_status_at: 2.years.ago)
+
+ run_worker
+
+ deleted_statuses = UserStatus.where(user_id: [user_status_1.user_id, user_status_2.user_id])
+ expect(deleted_statuses).to be_empty
+ end
+ end
+end
diff --git a/spec/workers/wikis/git_garbage_collect_worker_spec.rb b/spec/workers/wikis/git_garbage_collect_worker_spec.rb
new file mode 100644
index 00000000000..77c2e49a83a
--- /dev/null
+++ b/spec/workers/wikis/git_garbage_collect_worker_spec.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Wikis::GitGarbageCollectWorker do
+ it_behaves_like 'can collect git garbage' do
+ let_it_be(:resource) { create(:project_wiki) }
+ let_it_be(:page) { create(:wiki_page, wiki: resource) }
+
+ let(:statistics_service_klass) { Projects::UpdateStatisticsService }
+ let(:statistics_keys) { [:wiki_size] }
+ let(:expected_default_lease) { "project_wikis:#{resource.id}" }
+ end
+end